天天看點

android widget(桌面小部件)實作

本文介紹如何自己實作一個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已經複活,然後再給自己發廣播,我們就可以收到了。