本文介紹如何自己實作一個widget以及各種注意事項。
首先我們先來看一下widget支援的布局,這是第一個“坑”
widget支援的layout和view類型十分有限,隻支援下面幾種
FrameLayout
LinearLayout
RelativeLayout
GridLayout
And the following widget classes:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
ViewStub
也就是說,如果想繪制一條分割線,隻能在上面挑選一個view改變寬高設定背景。
如果想做一個toggle button,也隻能用imageview發送自定義廣播,通過接收廣播來改變狀态(至于為什麼不直接改變image,往後看)
接下來,我們來看widget的邏輯處理元件,AppWidgetProvider
AppWidgetProvider用來接收widget事件,然後根據不同僚件對widget進行不同處理。
AppWidgetProvider本質是一個廣播接收器,在onReceive方法中處理了一些系統關于widget的廣播,app隻需重寫相關方法即可
是以,在這裡你除了可以處理widget相關廣播,你也可以處理自定義廣播和系統其他廣播。
首先我們先在manifest中配置receiver
<receiver android:name=".TestWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
<action android:name="android.appwidget.action.APPWIDGET_DISABLED" />
<action android:name="com.test.widgetdemo.UPDATE_START" />
<action android:name="com.test.widgetdemo.UPDATE_END" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/app_widget"/>
</receiver>
meta-data中的resource指定了widget更多細節。
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.test.widgetdemo.WidgetCheckActivity"
android:initialLayout="@layout/widget"
android:minHeight="89dip"
android:minWidth="282dip"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1800000"/>
關鍵配置:
1. minHeight/minWidth cell數量與最小寬高的關系: 70 × n − 30 (機關dip)
2. 更新頻率 updatePeriodMillis, 最高為30分鐘/次, 超過30分鐘/次,系統自動重新指派
3. configure 當添加widget時, 打開此activity用于對widget進行檢查或配置。比如在這裡可以檢查登入狀态,賬戶設定等等。
public class WidgetCheckActivity extends Activity {
protected int mAppWidgetId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
Intent intent = getIntent();
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
} else {
mAppWidgetId = savedInstanceState.getInt("widgetId");
}
Toast.makeText(this, "pass", Toast.LENGTH_LONG).show();
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("widgetId", mAppWidgetId);
}
}
上面的代碼示範了如何擷取appWidgetId,如果有需要的話,可以将它在onSaveInstanceState中儲存。
當此activity工作完成後,需要傳回appWidgetId并關閉activity,否則widget添加不上。
view改變屬性、狀态和事件都無法使用view相關的方法,所有view隻能通過RemoteViews對象來操作。
RemoteViews是你的布局映射的一個對象,使用它,根據你的view id來改變view的屬性、狀态和事件。比如:
views.setTextViewText(textview_id, "文本");
views.setTextColor(textview_id, 文本顔色);
views.setImageViewResource(imageview_id, 圖檔);
views.setViewVisibility(view_id, 顯示狀态);
views.setOnClickPendingIntent(view_id, 單擊事件);
是以,之前說做一個toggle button的時候沒有直接通過事件改變背景或src,是因為這裡設定的并不是一個OnClickListener,而是一個PendingIntent。我們沒有辦法直接改變背景,隻能通過這個PendingIntent回來,然後再改變背景或者src。
還有一些屬性,RemoteViews沒有提供直接修改的方法,但提供包裝了反射的方法,比如改變背景:
views.setInt(view_id, "setBackgroundResource", resId);
相關的還有setLong/setShort/setBoolean/setChar/setByte/setString/setUri/setBitmap/setBundle/setIntent等等,不再一一列舉
AppWidgetProvider有幾個回調方法,這裡隻列舉幾個
添加widget
1.螢幕上第一個widget
系統會發送廣播ACTION_APPWIDGET_ENABLED
2.之後添加的widget不會有enable或者add之類的廣播. 但是每次添加widget依然會打開配置activity. 可以通過配置activity發送自己的初始化廣播.注意: 收到此廣播和系統的update廣播順序不定
删除widget
1.删除widget系統會發送ACTION_APPWIDGET_DELETED廣播
2.删除最後一個widget系統會發出ACTION_APPWIDGET_DISABLED廣播
更新widget時,系統會發送ACTION_APPWIDGET_UPDATE廣播
widget大小改變時,系統會發送ACTION_APPWIDGET_OPTIONS_CHANGED廣播
是以,如果要做一個重新整理按鈕,我們的步驟大概應該是這樣
首先布局,一個ImageView和一個ProgressBar
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|right">
<ImageView
android:id="@+id/refresh"
android:layout_width="17dip"
android:layout_height="17dip"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/app_widget_refresh"/>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="17dip"
android:layout_height="17dip"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/app_widget_progress"
android:indeterminateDuration="2000"
android:visibility="gone"/>
</FrameLayout>
然後初始化View狀态并設定事件監聽,然後更新widget
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] ids = getAppWidgetIds(context);
int N = ids.length;
for (int i = 0; i < N; i++) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
views.setOnClickPendingIntent(R.id.refresh, makeUpdateStartPendingIntent(context));
appWidgetManager.updateAppWidget(ids[i], views);
}
這裡make一個啟動Service的PendingIntent
private static PendingIntent makeUpdateStartPendingIntent(Context context) {
Intent intent = new Intent(context, WidgetHelperService.class);
intent.putExtra("action", ACTION_UPDATE_START);
return PendingIntent.getService(context, REQUEST_REFRESH, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
然後通過service再發送廣播,至于為什麼不直接發送廣播,稍後會告訴大家。
這是我們可以通過AppWidgetProvider來接收并處理此廣播,用于進行更新資料操作以及重新整理view
下面隻是模拟一下重新整理資料的動作,兩秒中後,重新整理按鈕恢複之前的狀态。
private static void showProgressBar(final Context context, boolean show) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] ids = getAppWidgetIds(context);
int N = ids.length;
for (int i = 0; i < N; i++) {
final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
if (show) {
views.setViewVisibility(R.id.progress_bar, View.VISIBLE);
views.setViewVisibility(R.id.refresh, View.GONE);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
context.sendBroadcast(new Intent(ACTION_UPDATE_END));
}
}, 2000L);
} else {
views.setViewVisibility(R.id.progress_bar, View.GONE);
views.setViewVisibility(R.id.refresh, View.VISIBLE);
}
appWidgetManager.partiallyUpdateAppWidget(ids[i], views);
}
}
當然,在這個例子裡,我們可以直接在run函數中操作view而不需要通過廣播
更新widget有兩個方法
AppWidgetManager.updateAppWidget(int appWidgetId, RemoteViews views)
AppWidgetManager.partiallyUpdateAppWidget(int appWidgetId, RemoteViews views)
注意:
1. 如果你發現,有時view事件無法相應,那麼你可能是在調用partiallyUpdateAppWidget之前沒有調用partiallyUpdateAppWidget, partiallyUpdateAppWidget在widget沒有調用updateAppWidget之前,它是無效的(實際部分版本有效,5.0+?)。
2. 如果你發現view事件還是有時無法相應,那麼你可能是通過PendingIntent.getBroadcast給view設定了一個發送廣播的PendingIntent,要注意的是,在強制停止app之後,widget中的view無法再發送廣播了,但是可以打開activity和service,是以我的做法是,通過view打開service,然後通過service發送廣播
最後來說一下,為什麼之前給view設定事件想發送廣播的時候,沒有直接使用PendingIntent.getBroadcast而是使用PendingIntent.getService
當app被強制停止後,高版本android中app是無法再接收廣播的,無論是以哪種注冊形式注冊接收器。有的廠商的rom殺死app後也是如此。是以我們隻能使用迂回戰術,使用者點選launcher中widget觸發事件打開我們自己的service,這時我們的app已經複活,然後再給自己發廣播,我們就可以收到了。