Android App Widgets總結

簡介

App Widgets 是一個可以嵌入到其他app(比如 HomeScreen)的能接收到週期性更新的一個微型的App 界面。這個view對用戶來說相當於一個小插件或者小部件。如果我們把這個小部件放到桌面的話,可以方便用戶不用打開app而能查看到重要的消息。

重要的類

AppWidgetProviderInfo

一個描述appWIdget的類,此類中的信息表示我們的app widget layout、最小高寬度或者更新頻率等等信息。

AppWidgetProvider

這個類非常重要,我們在開發的時候也會繼承它。這個類集成自broadcastreceiver,而且我們可以跳進它源碼中看到,在它的onReceive()裏面分發了很多事件,比如更新app widget會分發到onUpdate(),第一次添加app widget會分發到onEnable(),最後一個app widget刪除後,會調用onDisable()等等。

RemoteViews

一個用於遠程傳輸的view,其實它不是一個view,它會把我們的view和對該view的操作用action封裝起來,然後序列化傳輸到另外的進程,然後反序列化,然後再在另外的進程對view進行我們要進行的操作。

AppWidgetManager

連接app進程和我們widget所處的進程的一個橋樑。後面詳細講。

用法

我們知道,一個app widget是放在homeScreen或者鎖屏界面的,所以它實際上是運行在其他進程的,所以這裏得用到進程間通信。這也是爲什麼開發一個app widget很麻煩的原因。

AndroidManifest.xml

首先在我們的AndroidManifest.xml中聲明一個AppWidgetProvider用於接收app widget的更新事件。AppWidgetProvider實際上是一個廣播,所以註冊方式和廣播類似。

<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>

上面代碼中有一個meta-data,後面的name必須是android.appwidget.provider。因爲系統正是根據這個來找到我們widget的配置文件的,下面resource就是它的配置文件,它對應着上面的AppWidgetProviderInfo。我們在resource中配置的所有配置項都可以在這個類中找到。
xml/example_appwidget_info.xml:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>
  • android:updatePeriodMillis:更新時間(默認30min),每過一段時間,系統會給我們的AppWidgetProvider發送一個update消息,我們可以在AppWidgetProvider中的onUpdate()中來處理更新widget界面。
  • android:previewImage:預覽圖標,用戶在桌面選擇widget的時候可以看到這個圖標。和ic_launcher有點類似的感覺。
  • android:initialLayout:layout,不用說都知道
  • android:configure:配置。要是有需要的話,用戶在添加widget的時候會打開這個Activity。來自定義配置相關的參數。
  • android:resizeMode:widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
  • android:widgetCategory: widget可以顯示的地方,可以是桌面,也可以是鎖屏。

在添加configure的時候,我們也要在AndroidMainfest.xml中爲要打開的Activity設置intent-filter:

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

layout

由於widget不是跑在我們自己的進程,所以對其界面有限制,因爲系統會用到binder通信,把我們的傳到另外一個進程去。

widget現在支持的佈局有:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

支持的控件有:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper

其實RemoteViews也支持ViewStub

margin

在android4.0以前,我們得爲widget的邊緣添加margin,因爲它不能觸碰到屏幕邊界。
在android 4.0以後,系統默認爲我們添加了padding,所以我們在android4.0以後不用管。

AppWidgetProvider

我們的widget對應着AppWidgetProvider,我們在AppWidgetProvider中接收系統發給我們的更新、添加、刪除widget消息。

onUpdate()
我們可以在這個函數裏面做一些更新界面的事情。當updatePeriodMillis所設置的時候到達後,就會運行這個函數。

onEnable()
用戶添加第一個widget的時候會調用

onDisable()
和onEnable相反,刪除最後一個widget的時候調用。

onAppWidgetOptionsChanged()
widget尺寸改變的時候調用(用戶可以調整大小)。

onDeleted()
用戶每次刪除widget的時候調用

onReceive(Context, Intent)
上述函數都在這個函數裏面分發。也就是說,實際上系統是給app發的一個個廣播事件。在這個函數中根據不同的事件,調用上述不同的函數。

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;//該widget個數
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];
      //pendingIntent是一種還沒有發生的intent
      //我們可以用pendingIntent來爲view設置一個onClick後調用的Intent事件。
      //這裏是當用戶點擊了R.id.button按鈕後,用intent打開ExampleActivity。 
           Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            //用appwidgetManager來通知系統更新widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

上面代碼只是一點擊widget是某個button打開activity的例子。當然我們也可以在這裏用PendingIntent來設置一個事件,讓用戶點擊某個控件後可以向我們發送一個廣播,然後在接收到廣播後做我們想做的事情。

Intent intent = new Intent();
intent.addAction(xxx);
PendingIntent ppendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

當然,我們得在ExampleAppWidgetProvider加入相應的action。

到這裏,widget基本的操作已經完成。

Widget配合ListView等Collections使用

在我們日常開發中,ListView等Collections的使用是必不可少的,這一節就講一下關於如何在widget中使用collections,我這裏用listview爲代表,其他的都類似。

前面沒有Collections的時候,直接用RemoteViews就可以直接填充視圖,然後加上Listview後,你會發現,沒法來填充ListView裏面的Item。

這裏就要介紹下在用Collections的時候,其他幾個重要的類了:

  • RemoteViewsService :一個連接widget和app進程的service,我們可以繼承這個類,來提供一個adapter給ListView/GridView
  • RemoteViewsService.RemoteViewsFactory:一個類似BaseAdapter一樣的東西,我們可以直接把它看成ListView的Adapter。

有了這兩個類就好說了。首先RemoteViewsService 是一個Service,所以我們得在manifest.xml裏面註冊,而且得申明綁定widget的權限。

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

申明後,我們的ListView得這麼用(官方栗子):

public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
    //初始化listView並指定adapter
    for (int i = 0; i < appWidgetIds.length; ++i) {

        //先new一個intent來啓動service
        //這個service會提供給我們listview的item的界面。
        Intent intent = new Intent(context, MyWidgetService.class);
        //把app widget id 放到intent裏面
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
        //widget_layout.xml裏面包含一個listview
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
    
        //爲listview指定一個 remote adapter,這個adapter會用intent連接到一個特定的service(MyWidgetService)
        //我們後面就可以在service裏面來提供數據。
        rv.setRemoteAdapter(appWidgetIds[i], R.id.list_view, intent);
        //爲我們的listview指定一個empty view,這個empty view得也在上面的widget_layout裏面
        rv.setEmptyView(R.id. , R.id.empty_view);

            //這裏爲了讓我們的item可以設置點擊事件,我們得爲listview設置一個pending intent template(一個pendingIntent模板)
            //然後後面我們會在adapter裏面爲每個item設置一個fillInIntent
            Intent toastIntent = new Intent(context, ExampleAppWidgetProvider.class);
            //這裏設置了一個action。
            //ExampleAppWidgetProvider.TOAST_ACTION是我們自定義的一個action。這裏得把這個action添加到廣播接收者裏面。以便我們能接收到按下item後的廣播事件。
            toastIntent.setAction(ExampleAppWidgetProvider.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.list_view, toastPendingIntent);

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

下面我們來看看MyWidgetService

public class MyWidgetServiceextends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
    //爲widget提供一個adapter
        return new MyWidgetService(this.getApplicationContext(), intent);
    }
}

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

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

    // 初始化data
        public void onCreate() {
           
            //這裏不建議下載或者做一些很重的操作,因爲這裏停留20s,你就會收到一個ANR的獎勵。
            for (int i = 0; i < mCount; i++) {
                mWidgetItems.add(new WidgetItem(i + "!"));
            }
           ...
        }
        ...

      //相當於baseAdapter的getView()方法。
        public RemoteViews getViewAt(int position) {
           //這裏用layout創建一個RemoteVies。
            RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
            rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
            //這裏用fill-intent來填充前面設置的Intent模板
            Bundle extras = new Bundle();
            extras.putInt(ExampleAppWidgetProvider.EXTRA_ITEM, position);
            Intent fillInIntent = new Intent();
            fillInIntent.putExtras(extras);
            //爲item設置intent onClick。當點擊後,會發出一個特定intent的廣播。我們在ExampleAppWidgetProvider裏面接收廣播就可以做我們想做的事了。
            //但是這裏還是要記得在AndroidManifest.xml裏面ExampleAppWidgetProvider申明接收這個action。
            rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

            ...

            // 返回view
            return rv;
        }
    ...
    }

但是如果我們要更新listview中的數據的話,下面官方文檔裏面給出一一幅圖,很好地描述了工作流程,我們只要更新數據後,調用AppWidgetManager.notifyAppWidgetViewDataChanged()就可以了。

appwidget_collections.png

大坑提醒:要是在開發中發現程序跑出來的效果不是我們所想象的, 最好把app下載了,然後重新添加桌面小插件可能就解決問題了,我在實際開發的時候,被這個問題坑了好幾次==。

RemoteViews的工作原理

看了上面的開發流程,我們來大概分析一下它的工作原理。

RemoteViews主要用於通知欄和桌面小部件,然而通知欄和桌面小部件又是分別由NotificationManagerAppWidgetManager管理的。而我們在開發的時候,經常還很把它們和NotificationManagerService或者AppWidgetService一起使用。而我們在其他Android開發的時候,經常會遇到各種Manager。還有很多的Service,比如WindowManagerServiceWindowManager。用多了我們就會發現,它們的用法有很多相似的地方。

要是稍微觀察一下它們的工作機制的話,就會發現它們都是差不多的。

其實AppWidgetManager是通過BinderSystemServer進程中的AppWidgetService通信的。

由此可見,桌面小插件實際上是在AppWidgetService被加載的。而它們又是運行的SystemServer中,這就滿足了跨進程通信的條件。

我們發現RemoteViews實際上是實現了Parcelable接口的,所以它也是便於在Binder中進行跨進程傳輸的。

還記得我們在new RemoteViews的時候,是傳遞了一個包名進去的麼?
其實系統會根據RemoteViews中指定的包名去得到相應的資源,然後用LayoutInflater來加載佈局文件。這就在SystemServer中加載了一個View了。然後還記得我們對View的操作都是用set來完成的麼?比如:

rv.setTextViewText(R.id.text_view,"hello");

然後來看看源碼:

 public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }
    
 public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }

我們發現爲viewsetText的時候,本來平常view是通過view.setText(xxx)來完成的,剛好這裏把我們的id和方法setText還有text記錄下來,然後new了一個action.其實這裏的ReflectionAction就是繼承Action的。然後再addAction();

其實在RemoteViews裏面有一個ArrayList<Action>。addAction()會把我們的Action放到list裏面。當把這個RemoteViews傳輸到SystemServer的時候 ,系統會調用RemoteViewsapply()方法:

 public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;
    LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Clone inflater so we load resources from correct context and
        // we don't add a filter to the static version returned by getSystemService.
        inflater = inflater.cloneInContext(inflationContext);
        inflater.setFilter(this);
        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);
        return result;
        }

印證了剛剛說了用inflater加載佈局,然後再調用perforApply()方法:

    private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler);
            }
        }
    }

遍歷Action ,然後調用其apply()方法。我們來看看剛剛的ReflectionActionapply()方法:

  public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final View view = root.findViewById(viewId);
            if (view == null) return;

            Class<?> param = getParameterType();
            if (param == null) {
                throw new ActionException("bad type: " + this.type);
            }

            try {
                getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
            } catch (ActionException e) {
                throw e;
            } catch (Exception ex) {
                throw new ActionException(ex);
            }
        }

豁然開朗,用反射調用我們實際操作的view.setText()

首先序列化,再通過Binder傳遞對象,然後再在SystemServer中來apply我們的操作。這就完成了我們想要的操作,方法非常巧妙。

總結

跨進程操作很巧妙,我們可以通過學習google的做法,在我們實際開發當中,也有不少的跨進程操作,我們也可以用這種方式來實現操作其他對象。特別是在開放SDK中,這種方式非常有幫助,異步調用要執行的代碼。

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