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檢視所在的區域。