應用小部件(App Widget)---- 基礎篇(2)

文章內容


1. 實例代碼分析

 

         經過《應用小部件(App Widget)---- 基礎篇(1)》的介紹後,並對Android 4.0.3上的一個實例代碼進行

了部分修改,然後將其作爲例子進行具體地講解。

        實例代碼來源可以參考:http://developer.android.com/resources/samples/WiktionarySimple/index.html


1.  實例代碼分析

        

       正如《應用小部件(App Widget)---- 基礎篇(1)》中的第二部分(App Widgets的基本要素 )所述,在該

實例程序中可以很明顯區分出構成部件的基本組成要素。


       1.1  應用小部件提供器信息對象(AppWidgetProviderInfo object)

      

       在工程目錄res/xml/下的widget_word.xml文件中設置部件提供器信息對象的基本信息,內容如下:

 

       
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="146dip"
    android:minHeight="72dip"
    android:updatePeriodMillis="100000" 
    android:initialLayout="@layout/widget_message" 
    android:resizeMode="horizontal"
/>


         在此,我們設置了部件的大小尺寸,更新週期,初始佈局,及調整大小的模式。

        

        1.2  小部件視圖佈局(View layout)


        在工程目錄res/layout/下的widget_word.xml文件中定義了部件,內容如下:


       
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/widget"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    style="@style/WidgetBackground">

   <TextView 
        android:id="@+id/time_update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dip"
        style="@style/Text.Time"/>
    
    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:src="@drawable/star_logo" />

    <TextView
        android:id="@+id/word_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/time_update"
        android:layout_marginBottom="1dip"
        android:includeFontPadding="false"
        android:singleLine="true"
        android:ellipsize="end"
        style="@style/Text.WordTitle" />

    <TextView
        android:id="@+id/word_type"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/word_title"
        android:layout_toLeftOf="@id/icon"
        android:layout_alignBaseline="@id/word_title"
        android:paddingLeft="4dip"
        android:includeFontPadding="false"
        android:singleLine="true"
        android:ellipsize="end"
        style="@style/Text.WordType" />

    <TextView
        android:id="@+id/show_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/word_title"  
        android:paddingRight="4dip"
        android:includeFontPadding="false"
        android:singleLine="true"
        style="@style/BulletPoint" />
         
    <TextView
        android:id="@+id/definition"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/word_title"
        android:layout_toRightOf="@id/show_id"
        android:paddingRight="5dip"
        android:paddingBottom="4dip"
        android:includeFontPadding="false"
        android:lineSpacingMultiplier="0.9"
        android:maxLines="4"
        android:fadingEdge="vertical"
        style="@style/Text.Definition" />
   
</RelativeLayout>

        這個小部件裏主要包括了顯示更新時間的視圖元素,標題視圖元素,標題的定義視圖元素,顯示部件實例ID的視圖元素及一個小圖標視圖元素。


       1.3  小部件提供器(AppWidgetProvider)類


       該類的定義如下:


        

package com.example.android.simplewiktionary;

import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.IBinder;
import android.os.SystemClock;
import android.text.format.DateFormat;
import android.text.format.Time;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.example.android.simplewiktionary.SimpleWikiHelper.ApiException;
import com.example.android.simplewiktionary.SimpleWikiHelper.ParseException;

/**
 * Define a simple widget that shows the Wiktionary "Word of the day." To build
 * an update we spawn a background {@link Service} to perform the API queries.
 */
public class WordWidget extends AppWidgetProvider 
{
	
	public final static String  MARK_WIDGET_ID = "appWidgetId";
	public final static String  ACTION_UPDATE = "android.test.appwidget.action_updated";
	public Intent intent;
	
	public  void onUpdateWidgets(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
	{
		final int N = appWidgetIds.length;
		
    	Log.w("wanjf", "Id count is :" + String.valueOf(N));
		for (int i = 0; i < N; i++) 
   	 	{
            int appWidgetId = appWidgetIds[i];
            String strId = String.format("This widget's id = %d ", appWidgetId);
            Intent intent = new Intent(context, UpdateService.class);
            appWidgetManager.getAppWidgetInfo(appWidgetId).updatePeriodMillis = 0;
            intent.putExtra(MARK_WIDGET_ID, appWidgetId);
            context.startService(intent);
            Log.w("wanjf", strId);
   	 	}
	}
	
	@Override
	public void onReceive(Context context, Intent intent)
	{
		Log.w("wanjf","onReceive is called!");
		this.intent = intent;
		
		if(ACTION_UPDATE.equals(intent.getAction()))
		{
			AppWidgetManager gm = AppWidgetManager.getInstance(context);
			ComponentName thisWidget = new ComponentName(context, WordWidget.class);
			int[] appWidgetIds  = gm.getAppWidgetIds(thisWidget);
			Log.w("wanjf", "Recieve the broadcast !!");
			Log.w("wanjf", "Widgets' count is:" + String.valueOf(appWidgetIds.length));
			onUpdateWidgets(context, gm, appWidgetIds);
		}
		else
		{
			//
			super.onReceive(context, intent);
			//
		}
	}
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) 
    {
    	 Log.w("wanjf","onUpdate is called!");
    	 onUpdateWidgets(context, appWidgetManager, appWidgetIds);
    	 super.onUpdate( context,  appWidgetManager, appWidgetIds); 
    }
    
    @Override
    public void onDeleted(Context context,int[] widgetIds)
    {
    	Log.w("wanjf","onDeleted is called!");
    	//
    	CharSequence prompt = "Oh! Widget id : " + String.valueOf(widgetIds[0]) + " id deleted.";
    	Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();
    	//
    	context.stopService(new Intent(context, UpdateService.class));
    	// TODO Auto-generated method stub
    	super.onDisabled(context);
    }

    @Override
	public void onDisabled(Context context)
    {
    	Log.w("wanjf","onDisabled is called!");
		//
		AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
	  	PendingIntent sender = PendingIntent.getBroadcast (context, 0, new Intent(ACTION_UPDATE), 0);
		am.cancel(sender);
		//
		CharSequence prompt = "Oh! I am the last Widget";
    	Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();	
    	// TODO Auto-generated method stub
    	super.onDisabled(context);
    	//
	}

	@Override
	public void onEnabled(Context context) 
	{
		Log.w("wanjf","onEnabled is called!");
		
		AppWidgetManager gm = AppWidgetManager.getInstance(context);
		ComponentName thisWidget = new ComponentName(context, WordWidget.class);
		int[] appWidgetIds  = gm.getAppWidgetIds(thisWidget);
		//
		PendingIntent sender = PendingIntent.getBroadcast (context, 0, new Intent(ACTION_UPDATE), 0);
	    // We want the alarm to go off 30 seconds from now.
	    long firstTime = SystemClock.elapsedRealtime();
	    firstTime += 20*1000;
	    // Schedule the alarm!
	    AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
	    am.setRepeating(AlarmManager.ELAPSED_REALTIME,firstTime, 15*1000, sender);
	    //
	    CharSequence prompt = "Oh! I am the first Widget id : " + String.valueOf(appWidgetIds[0]);
	    Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();
		// TODO Auto-generated method stub
		super.onEnabled(context);
		//
	}

	public static class UpdateService extends Service 
	{
        @Override
        public void onStart(Intent intent, int startId) 
        {
        	Log.w("wanjf","Service's onStart() is called!");
            // Build the widget update for today
        	int appWidgetId = intent.getIntExtra(MARK_WIDGET_ID, -1);
            RemoteViews updateViews = buildUpdate(this, appWidgetId);
           
            if(appWidgetId == -1)
            	stopSelf();
            // Push update for this widget to the home screen
            //ComponentName thisWidget = new ComponentName(this, WordWidget.class);
            AppWidgetManager manager = AppWidgetManager.getInstance(this);
            //manager.updateAppWidget(thisWidget, updateViews);
            manager.updateAppWidget(appWidgetId, updateViews);
        }

        /**
         * Build a widget update to show the current Wiktionary
         * "Word of the day." Will block until the online API returns.
         */
        public RemoteViews buildUpdate(Context context, int widgetId)
        {
            // Pick out month names from resources
            Resources res = context.getResources();
            String[] monthNames = res.getStringArray(R.array.month_names);

            // Find current month and day
            Time today = new Time();
            today.setToNow();

            // Build today's page title, like "Wiktionary:Word of the day/March 21"
            String pageName = res.getString(R.string.template_wotd_title, monthNames[today.month], today.monthDay);
            //Log.w("wanjf",pageName);
            RemoteViews updateViews = null;
            String pageContent = "";

            try 
            {
                // Try querying the Wiktionary API for today's word
                SimpleWikiHelper.prepareUserAgent(context);
                pageContent = SimpleWikiHelper.getPageContent(pageName, false);
                Log.w("wanjf",pageContent);
            } 
            catch (ApiException e) 
            {
                Log.e("WordWidget", "Couldn't contact API", e);
            } 
            catch (ParseException e)
            {
                Log.e("WordWidget", "Couldn't parse API response", e);
            }

            // Use a regular expression to parse out the word and its definition
            Pattern pattern = Pattern.compile(SimpleWikiHelper.WORD_OF_DAY_REGEX);
            Matcher matcher = pattern.matcher(pageContent);
            if (matcher.find()) 
            {
            	
            	CharSequence time = DateFormat.format("hh:mm:ss", Calendar.getInstance()) ;
                // Build an update that holds the updated widget contents
                updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_word);

                String wordTitle = matcher.group(1);
                Log.w("wanjf",wordTitle);
                updateViews.setTextViewText(R.id.word_title, wordTitle);
                updateViews.setTextViewText(R.id.word_type, matcher.group(2));
                updateViews.setTextViewText(R.id.definition, matcher.group(3).trim());
                updateViews.setTextViewText(R.id.show_id, "[" + String.valueOf(widgetId) + "]");
                updateViews.setTextViewText(R.id.time_update, time);
                // When user clicks on widget, launch to Wiktionary definition page
                String definePage = res.getString(R.string.template_define_url,Uri.encode(wordTitle));
                Intent defineIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(definePage));
                PendingIntent pendingIntent = PendingIntent.getActivity(context,
                        0 /* no requestCode */, defineIntent, 0 /* no flags */);
                updateViews.setOnClickPendingIntent(R.id.widget, pendingIntent);

            } 
            else
            {
                // Didn't find word of day, so show error message
                updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_message);
                CharSequence errorMessage = context.getText(R.string.widget_error);
                updateViews.setTextViewText(R.id.message, errorMessage);
            }
            return updateViews;
        }

        @Override
        public IBinder onBind(Intent intent) 
        {
            // We don't need to bind to this service
            return null;
        }
    }
}

         

       在原始代碼的基礎上,我們做了如下的修改:

       

                 1.  重寫了基類appWidgetProvider的所有方法;

                 2.  設置警報來更新小部件;

                 3.  增加對部件進行操作的提示;

                 4.  對某些操作(警報,服務)的設置和清理做了控制;

          

          

         接下來,我們重點對上述做了修改的部分進行分析

        

       修改後的代碼重寫了基類的onReceiveonUpdateonDeletedonDisabledonEnabledonUpdate

方法。有了這些方法,我們便很容易發現當他們被調用時在部件上究竟發生了什麼。


         首先,當我們把第一個部件實例添加到宿主視圖內時,onReceive方法首先被調用,然後是onEnabled方法,接

着還是onReceive方法,最後是onUpdate方法。由於我們在onEnabled內設置了一個重複觸發的警報,因此當警報

在隨後被觸發時,還將調用onReceive方法。

       

         提醒:由於onEnabled方法只在部件實例的首次被創建時纔會被調用,因此在此方法內執行一些對所有部件實例都適用的操作是最佳場所。

         接着,當繼續向宿主視圖內添加一個(或多個)該部件的實例時,首先調用的是onReceive方法,然後是

onUpdate方法。

     

         然後,從宿主內刪除一個該部件的實例時,首先調用的是onReceive方法,然後是onDeleted方法。

       最後,當該部件的所有實例從宿主內被移除時,首先調用的是onReceive方法,然後是onDeleted方法,接着由

onReceive方法,最後是onDisabled方法。


       提醒:onDisabled方法也只被調用一次,因此也是執行清理操作的最佳場所。此例中,在該方法內取消了警報,停止服務。

        

         由於我們重寫了基類的諸方法,因此別忘記調用該方法的基類版本。尤其是在重寫onReceive方法後,如果忘記

調用基類版本的onReceive,我們重寫其他方法也不會被調用。除非在重寫的onReceive方法對收到的廣播進行過濾

後再手動調用其他方法。


       除了以上方法外,我們需要留意自定義方法onUpdateWidgets。在該方法內我們啓動了一個“服務”來執行部

件的更新(主要是更細其上的顯示內容)。之所以這麼做主要是因爲部件上的內容中有些是自於網絡上的數據,而對

於網絡數據的請求可能耗時幾秒鐘,甚至更多。在服務內執行網絡數據請求可以保證WordWidget的onReceive及時

返回。否則,可能會因爲ANR錯誤,活動管理器在後臺將其結束掉(Kill)。


       此例中,“服務”內除了獲取網絡數據外,還對部件與用戶接口上的交互進行了設置。於是當用戶點擊部件時,

將會打開相應的網頁。


         現在我們來看一下運行後的效果:

                                    

                                     

          

         Home Screen(宿主)內的這一大一小兩個部件都是我們剛剛創建的,由於在部件提供器信息對象對應的xml文

件內設置了 android:resizeMode="horizontal",因此我們可以在水平方向上調整部件的大小。除此,我們需要留意

在部件上的藍色數字和紅色數字。藍色數字是每個部件實例最後一次更新的時間,而紅色數字是該部件的實例ID。


        

        部件的實例id是由部件管理器維護的一組數字,表示每個實例對應一個不同的id,因此在更新時可以獲取他們並

對其代表的部件實例進行更新。

      

       通過觀察部件上時間變化可知,每個實例上的時間變化並不是一致進行的。這是因爲在onUpdateWidgets方法內通過迭代id數組後,再使用指定的id對其表示的實例進行更新。如果要是所有實例上的時間同時更新,可以將onUpdateWidgets方法修改成:


 public  void onUpdateWidgets(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)  
    {  
                Intent intent = new Intent(context, UpdateService.class);  
                context.startService(intent);  
    }


       然後再將內部類UpdateService的onStart方法修改成:      


  public void onStart(Intent intent, int startId)   
    {  
              Log.w("wanjf","Service's onStart() is called!");  
               // Build the widget update for today  
               RemoteViews updateViews = buildUpdate(this, appWidgetId);  
               updateViews.setViewVisibility(R.id.show_id, View.INVISIBLE);  
               // Push update for this widget to the home screen  
               ComponentName thisWidget = new ComponentName(this, WordWidget.class);  
               manager.updateAppWidget(thisWidget, updateViews);  
    }  
 


      

       至此,創建一個簡單的小部件程序就分析到這裏。當然,在此實例代碼裏還有涉及到網絡編程及正則表達式方面的內容。關於網絡的那部分代碼可以參考Android SDK上的介紹來理解具體接口的用法和作用。而關於正則表達的理解可以參考:http://www.java3z.com/cwbwebhome/article/article8/Regex/Java.Regex.Tutorial.html#note01


                                                                                                                                            2012年4月27日,畢



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章