1. 程式人生 > >Android App Widget

Android App Widget

原文
App Widget是一種可以被放在其他應用中(如Launcher)並接收週期性更新的應用檢視。這些檢視在UI上就表現為Widget,並且你可以同App Widget Provider一起釋出。
對於能夠包含其他App Widget的應用程式元件,稱為App Widget Host。
基本資訊

要建立一個App Widget,你需要完成以下步驟:
lAppWidgetProviderInfo物件:它描述了App Widget的基本元素,比如說佈局、更新頻率、AppWidgetProvider類等。這些都是在xml檔案中定義的。
l AppWidgetProvider類的實現:它定義了一些基本的方法以支援通過廣播事件與App Widget互動。通過它,當App Widget被更新、啟用、禁用以及刪除時,你將收到相應的廣播資訊。
l View Layout:通過xml檔案定義App Widget的初始檢視。
另外,你還可以實現一個App Widget的配置Activity。當然,這不是強制的。
在AndroidManifest中宣告一個App Widget

首先,宣告AppWidgetProvider。

Xml程式碼

<receiver android:name="ExampleAppWidgetProvider" >  
    <intent-filter>  
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />  
    </intent-filter>  
    <meta-data android:name="android.appwidget.provider"  
               android:resource
="@xml/example_appwidget_info" />
</receiver>

標籤用於指明App Widget使用的AppWidgetProvider
標籤必須包括一個含有android:name屬性的標籤。該屬性用於指明AppWidgetProvider接收APPWIDGET_UPDATE廣播。這是你唯一需要顯示宣告的廣播。當有需要時,AppWidgetManager自動傳送AppWidgetProder所需的各種廣播。
標籤標識了AppWidgetProviderInfo資源,它需要以下屬性:
l android:name:使用android.appwidget.provider來標識AppWidgetProviderInfo。
l android:resource:標識AppWidgetProviderInfo的資源位置。
新增AppWidgetProviderInfo元資料

AppWidgetProviderInfo定義了一個App Widget的必要屬性,例如最小布局範圍、初始佈局、更新頻率、以及在建立時顯示的配置Activity(可選)。
AppWidgetProviderInfo使用標籤來定義,並儲存在res/xml資料夾中。
Xml程式碼

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"  
    android:minWidth="294dp"  
    android:minHeight="72dp"  
    android:updatePeriodMillis="86400000"  
    android:previewImage="@drawable/preview"  
    android:initialLayout="@layout/example_appwidget"  
    android:configure="com.example.android.ExampleAppWidgetConfigure"   
    android:resizeMode="horizontal|vertical">  
</appwidget-provider>

l minWidth與minHeight屬性表示了App Widget所需的最小布局區域。
預設的主屏中,App Widget要想確認其位置,需要通過基於網格的具有固定寬度和高度的單元。如果App Widget的最小寬度和高度無法匹配給定的單元,它將會自動擴充套件到最接近的單元大小。
由於主屏的佈局方向是可變的,你應該考慮最壞的情況(每單元的寬和高都是74dp)。然而,為了防止在擴充套件時產生整數計算錯誤,你還需要減去2。因此,你可以用以下公式來計算最小寬度和高度(單位dp):(單元數量×74)-2。
同時,為了保證你的App Widget能夠在各種裝置上正常使用,它們的寬度和高度必須不超過4×4個單元。
lupdatePeriodMillis屬性定義了App Widget框架呼叫AppWidgetProvider的onUpdate方法的頻率。對於實際的更新,我們不建議採用該值進行實時處理。最好是越不頻繁越好——為了保證電量,一小時不超過一次為好。當然,你也可以允許使用者對更新頻率進行設定。
注意,如果更新觸發時裝置正處於休眠狀態,裝置將喚醒以執行該操作。如果你的更新頻率不超過一小時一次,這不會對電池的壽命產生多大的影響。但如果你需要更頻繁地更新卻又不想要在裝置休眠時執行,那你可以使用定時器來執行更新。要達到這種目的,可以在AlarmManager中設定一個AppWidgetProvider能接收的Intent。將型別設為ELAPSED_REALTIME或RTC。由於AlarmManager只有當裝置處於喚醒狀態時才會被呼叫,我們只要設updatePeriodMillis為0即可。
linitialLayout屬性標識了初始佈局檔案。
lconfigure屬性定義了當使用者新增App Widget時呼叫的Activity。(這是可選的)
lpreviewImage定義了App Widget的縮圖,當用戶從widget列表中選擇時,顯示的就是這張圖。如果沒設定,使用者將看見的是你的應用的預設圖示。
lautoAdvanceViewId屬性是在Android3.0引入的,用於標識需要被host(launcher)自動更新的widget的子檢視。
l resizeMode屬性標識了widget重新佈局的規則。你可以使用該屬性來讓widget能夠在水平、豎直、或兩個方向上均可變化。可用的值包括horizontal、vertical、none。如果是想在兩個方向上均能拉伸,可設定為horizontal|vertical,當然,需要Android3.1以上版本。
建立App Widget的佈局

要建立你的App Widget的初始佈局,你可以使用以下View物件。
建立佈局不是很麻煩,重點是,你必須記住,這個佈局是基於RemoteViews的,不是所有的佈局型別與View都支援。
一個RemoteViews物件可以支援以下佈局類:
FrameLayout
LinearLayout
RelativeLayout
以及一下widget類
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
注:這些類的子類是不支援的。
為App Widget新增邊距
作為一個widget,它不應該擴充套件到螢幕的邊緣,同時在視覺上,不應該與其它widget相混淆。因此,你需要為你的widget在四個方向上新增邊距。
在Android4.0中,所有的widget都自動添加了內邊距,並且提供了更好的對齊方案。要利用這種優勢,建議設定應用的targetSdkVersion到14或更高。
為了支援以前的裝置,你也可以為早些的平臺寫一個包含邊距的佈局,而對於Android4.0以上的,則不設定:
1. 設定targetSdkVersion到14或更高
2. 建立一個佈局:
Xml程式碼

<service android:name="MyWidgetService"  
...  
android:permission="android.permission.BIND_REMOTEVIEWS" /> 

Collection的佈局
佈局中最主要的部分就是collection檢視:ListView,GridView,StackView或AdapterViewFlipper。以下是一個例子:
Xml程式碼

<?xml version="1.0" encoding="utf-8"?>  

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent">  
    <StackView xmlns:android="http://schemas.android.com/apk/res/android"  
        android:id="@+id/stack_view"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:gravity="center"  
        android:loopViews="true" />  
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
        android:id="@+id/empty_view"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:gravity="center"  
        android:background="@drawable/widget_item_background"  
        android:textColor="#ffffff"  
        android:textStyle="bold"  
        android:text="@string/empty_view_text"  
        android:textSize="20sp" />  
</FrameLayout>  

注意:empty view是當沒資料時顯示的檢視。
除了App Widget的整體檢視,你還需要為collection中的每一項定義佈局。比如說,StackView Widget的例子中,只有一種。而WeatherListWidget的例子卻有兩種佈局檔案。
帶有collection的AppWidgetProvider
與普通App Widget的唯一區別就是,在帶有collection的AppWidgetProvider.onUpdate中,你需要呼叫setRemoteAdapter方法。通過呼叫這個方法,collection就知道如何獲取它的資料。然後你在RemoteViewsService中返回一個RemoteViewsFactory,這樣widget就能獲取到相應的資料了。另外,當你呼叫setRemoteAdapter時,你需要傳入一個Intent。這個Intent指向了RemoteViewsService的實現,並指明瞭App Widget的ID。
Java程式碼

public void onUpdate(Context context, AppWidgetManager appWidgetManager,  
int[] appWidgetIds) {  
    // update each of the app widgets with the remote adapter  
    for (int i = 0; i < appWidgetIds.length; ++i) {  

        // Set up the intent that starts the StackViewService, which will  
        // provide the views for this collection.  
        Intent intent = new Intent(context, StackWidgetService.class);  
        // Add the app widget ID to the intent extras.  
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);  
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));  
        // Instantiate the RemoteViews object for the App Widget layout.  
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);  
        // Set up the RemoteViews object to use a RemoteViews adapter.   
        // This adapter connects  
        // to a RemoteViewsService  through the specified intent.  
        // This is how you populate the data.  
        rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);  

        // The empty view is displayed when the collection has no items.   
        // It should be in the same layout used to instantiate the RemoteViews  
        // object above.  
        rv.setEmptyView(R.id.stack_view, R.id.empty_view);  

        //  
        // Do additional processing specific to this app widget...  
        //  

        appWidgetManager.updateAppWidget(appWidgetIds[i], rv);     
    }  
    super.onUpdate(context, appWidgetManager, appWidgetIds);  
}  

RemoteViewsService類

就像上面所說的,你的RemoteViewsService指向了用於建立遠端collection檢視的RemoteViewsFactory。
你需要執行以下操作:
1、 繼承RemoteViewsService
2、 在你的RemoteViewsService子類裡,實現一個RemoteViewsFactory的內部類。
注意:你不能依賴一個Service的實力來儲存資料。除非是靜態資料,否則任何內容都不應該儲存在此處。如果你要儲存你的App Widget的資料,你可以使用ContentProvider。
RemoteViewsFactory介面
你的實現了RemoteViewsFactory的類為App Widget提供了collection中的各項所需的資料。
其中,最重要的兩個需要實現的方法是onCreate和getViewAt。
當系統第一次建立factory物件時,會呼叫onCreate。在這裡面,你可以配置資料來源。通過建立內容集合或Cursor等等。例如,在StackView Widget中,onCreate裡建立了一個WidgetItem的陣列。當你的App Widget處於啟用狀態時,系統會使用它們的index來獲取資料並顯示。
Java程式碼

class StackRemoteViewsFactory implements  
RemoteViewsService.RemoteViewsFactory {  
    private static final int mCount = 10;  
    private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();  
    private Context mContext;  
    private int mAppWidgetId;  

    public StackRemoteViewsFactory(Context context, Intent intent) {  
        mContext = context;  
        mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,  
                AppWidgetManager.INVALID_APPWIDGET_ID);  
    }  

    public void onCreate() {  
        // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,  
        // for example downloading or creating content etc, should be deferred to onDataSetChanged()  
        // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.  
        for (int i = 0; i < mCount; i++) {  
            mWidgetItems.add(new WidgetItem(i + "!"));  
        }  
        ...  
    }  
...  

getViewAt用於返回特定位置上的RemoteViews物件。
Java程式碼

public RemoteViews getViewAt(int position) {  

    // Construct a remote views item based on the app widget item XML file,   
    // and set the text based on the position.  
    RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);  
    rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);  

    ...  
    // Return the remote views object.  
    return rv;  
}  

為單獨的item設定相應的行為

前文說過,通常來說,你是使用setOnClickPendingIntent方法來設定一個控制元件的點選事件。但對於collection中的子項,該方法是無效的。你可以先用setPendingIntentTemplate方法為collection整體的點選設定一個處理的PendingIntent,然後通過RemoteViewsFactory使用setOnClickFillInIntent為collection檢視中的每一項傳入一個與該項相關的Intent。該Intent會被合入處理時接收到Intent中。
在onUpdate中設定pending intent template

Java程式碼

// This section makes it possible for items to have individualized behavior.  
// It does this by setting up a pending intent template. Individuals items of a collection  
// cannot set up their own pending intents. Instead, the collection as a whole sets  
// up a pending intent template, and the individual items set a fillInIntent  
// to create unique behavior on an item-by-item basis.  
Intent toastIntent = new Intent(context, StackWidgetProvider.class);  
// Set the action for the intent.  
// When the user touches a particular view, it will have the effect of  
// broadcasting TOAST_ACTION.  
toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);  
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);  
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));  
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,  
    PendingIntent.FLAG_UPDATE_CURRENT);  
rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);  

在RemoteViewsFactory中設定fill-in intent
Java程式碼

public RemoteViews getViewAt(int position) {  
    // position will always range from 0 to getCount() - 1.  

    // Construct a RemoteViews item based on the app widget item XML file, and set the  
    // text based on the position.  
    RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);  
    rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);  

    // Next, set a fill-intent, which will be used to fill in the pending intent template  
    // that is set on the collection view in StackWidgetProvider.  
    Bundle extras = new Bundle();  
    extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);  
    Intent fillInIntent = new Intent();  
    fillInIntent.putExtras(extras);  
    // Make it possible to distinguish the individual on-click  
    // action of a given item  
    rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);   
    ...   
    // Return the RemoteViews object.  
    return rv;  
}  

保證Collection的資料是最新的

下圖給出了當更新發生時,一個帶有collection檢視的App Widget的工作流。
這裡寫圖片描述

帶有collection檢視的App Widget的一個作用就是為使用者提供實時的資料。例如,Android 3.0上的Gmail app widget,它為洪湖提供了他們的收件箱的快照。要實現這個功能,你需要能夠觸發你的RemoteViewsFactory及collection檢視去獲取以及顯示新的資料。你可以通過呼叫AppWidgetManager的notifyAppWidgetViewDataChanged方法來達到這個目的。
通過呼叫這個方法,RemoteViewsFactory的onDataSetChanged方法將被呼叫,你可以在其中進行資料的獲取。
onDataSetChanged以及後續的getViewAt方法中,你都可以進行一些處理密集型的操作。也就是說,你不用怕這個操作會佔用太長的時間,從而導致UI執行緒無響應。
如果getViewAt方法耗時太長,載入檢視(可由RemoteViewsFactory的getLoadingView獲取)將顯示在collection檢視所在的區域。