AppWidgets

App Widgets

App Widgets是一类视图较小的应用程序,它们可以内嵌在其它应用程序中(比如主屏)并 接收定时更新。在用户接口中,这类widget是以一些view视图呈现的,我们可以使用App Widget provider表述一个这种widget。可以内嵌App Widgets的应用程序组件称作App Widget host。下图是一个Music App Widget的截屏。


本篇介绍如何使用App Widget provider编写一个App Widget。有关创建自己的AppWidgetHost的讨论,参见App Widget Host部分。

Widget Design

更多有关app widget设计的信息,请阅读Widgets设计向导部分。

The Basics

创建一个App Widget,需要如下内容:

AppWidgetProviderInfo对象——描述App Widget的metadata,例如App Widget's的layout,更新频率和AppWidgetProvider类。这需要定义在XML文件中。

AppWidgetProvider类实现——定义了我们和App Widget交互的基于广播事件的接口方法。通过它,接收App Widget更新,enabled,disabled和删除时的广播。

View layout——定义了App Widget的初始layout。定义在XML文件中。

除此之外,我们还可以定义一个App Widget的配置Activity。这是一个可选的Acitivity提供给用户在创建App Widget的时候修改App Widget的设置。

Declaring an App Widget in the Manifest

首先,在AndroidManifest.xml文件中申明一个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>


<receiver>标签需要android:name属性,指明这个App Widget使用的 AppWidgetProvider

<intent-filter>标签必须报口一个有android:name属性的<action>标签。这个属性指明AppWidgetProvider接收ACTION_APPWIDGET_UPDATEG广播。这是我们唯一需要明确指明的广播。AppWidgetManager自动在需要的时候向AppWidgetProvider发送App Widget相关广播。

<meta-data>标签指定AppWidgetProviderInfo资源,它要求下列属性:

  • android:name-metadata的名字,使用android.appwidget.provider确认AppWidgetProviderInfo描述的数据。
  • android:resource-指明AppWidgetProviderInfo资源位置。
Adding the AppWidgetProviderInfo Metadata
AppWidgetProviderInfo定义了一个App Widget的必要信息。例如它的最小layout尺寸,初始layout资源,更新频率以及一个可选的在创建时期出现的配置activity。使用<appwidget-provider>标签在XML资源中定义AppWidgetProviderInfo对象,该文件保存到res/xml/文件夹中。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
       android:minWidth="40dp"
       android:minHeight="40dp"
       android:updatePeroidMillis="86400000"
       android:previewImage="@drawable/preview"
       android:initialLayout="@layout/example_appwidget"
        android:configure="com.example.android.ExampleAppWidgetConfigure"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen|keyguard"
        android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

<appwidget-provider>属性总结:
  • minWidth和minHeight-指定了初始状态下App Widget消费的最小数量空间。主屏定义了网格但愿定义了宽和高,如果App Widget的最小width和height不匹配网格尺寸,那么App Widget的尺寸取舍为最接近的网格尺寸。查看App Widget Design Guidelines获取更多App Widget尺寸相关信息。
        Note:如果想让这个app widget应用能跨终端使用,那么它的最小尺寸永远不要大于4x4网格。
  • minResizeWidth和minResizeHeight-指定App Widget的绝对最小尺寸。这些值所定义的尺寸的意思是小于它们的时候App Widget将比较模糊或不可使用。这组属性允许用户重定义widget的尺寸,但是这组尺寸必须比minWidth和minHeight定义的缺省尺寸小。Android3.1引入。同样查看App Widget Design Guidelines获取更多App Widget尺寸相关信息。
  • updatePeriodMillis-App Widget framework要求AppWidgetProvider调用onUpdate()回调方法更新app Widget。这个定义值不能保证更新即使发生,我们建议更新越少越好——为了省电,可能少于1次/h比较好。还有可能我们允许用户在配置信息中调整更新频率——有些人希望stock ticker15分钟更新一次,有些人可能希望一天只更新四次。
        Note:如果更新的时间到来的时候设备正处于休眠状态,设备会解除休眠进行更新。如果不是一小时更新一次(或多余一次),这或许不会对电池寿命产生较大影响。如果,我们需要频繁更新或当设备休眠时不需要更新,那么我们可以基于一个闹钟而不解除休眠的进行更新。这样作需要使用Intent设置一个闹钟,使用AlarmManager。把alarm的类型设置为ELAPSED_REALTIME或者RTC。那么只有在设备解除休眠的时候才会传递闹钟。然后设置updatePeriodMillis为0。
  • initialLayout-定义了App Widget的layout资源。
  • configure-定义了用户添加App Widget时启动的配置activity。这一项是可选项。
  • previewImage-指定了当用户选择了该app widget后的视图。如果未指定,那么只有该应用程序启动器的图标。这一部分与AndroidManifest.xml文件中<receiver>标签下的android:previewImage属性交互。更多使用previewImage的讨论信息,查看Setting a Preview Image部分。
  • autoAdvanceViewId-指定了app widget子视图id,它要被widget host主动激发。
  • resizeMode-指定该widget尺寸重定义的方式。horizontally,vertically表明在某个方向上可重定义,两个方向都可以的话使用horizontal|vertical。
  • minResizeHeight-当这个值比minHeight大或resizeMode中不支持vertical那么它无效。
  • minResizeWidth-同上类似
  • widgetCategory-申明是否可以显示在home screen上,lock screen上或都可。值为home_screen和keyguard。都可时需要保证它遵循所有widget类的设计规则。更多消息查看Enabling App Widgets on the Lockscreen。缺省值为home_screen。这一属性在Android4.2引入。
  • initialKeyguardLayout-它指定义在lock screen上的app widget layout的layout。它工作的方式同android:initialLayout,Android4.2引入。
查看AppWidgetProviderInfo类获取更多<appwidget-provider>标签属性信息。
Creating the App Widget Layout
必须定义一个initial layout存储在res/layout/文件夹中。使用下述类对象设计App Widget,同时一定要理解App Widget Design Guidelines中的设计规则。
创建App Widget layout同其它layout一样。但是需要明确的是App Widget基于RemoteViews,它不支持所有类型的layout和view控件。
它支持的layout类如下:
FrameLayout LinearLayout RelativeLayout GridLayout
它支持的widget类如下:
AnalogClock Button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFlipper
同时不支持上述类的继承类。
RemoteViews同时支持ViewStub,它是一个不可件的,零尺寸的View视图可以用来在运行时填充layout资源。
Adding margins to App Widgets
Widgets不应该扩展到屏幕边界,也不要和其它widget冲突,所以需要在widget的所有边界上添加margins。
Android4.0中,app widgets在widget frame和app widget bounding box之间添加padding。因此使用这种健壮的方式,推荐设置应用的targetSdkVersion为14或更高。
早期版本的独立layout应用自定义margins,Android4.0之后没有额外的margin。
1.设置应用程序的targetSdkVersion为14或更高。
2.如下所示:

<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="@dimen/widget_margin">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:background="@drawable/my_widget_background">
    …
  </LinearLayout></FrameLayout>
3.在res/values/文件夹中提供Android4.0之前的自定义margin。res/values-v14文件夹中为Android4.0 widgets定义无额外padding.
res/values/dimens.xml:
<dimen name="widget_margin">8dp</dimen>

res/values-v14/dimens.xml
<dimen name="widget_margin">0dp</dimen>

另一种选择是简单的在nine-patch背景图中构建margin。那么在API level14或更高版本中提供不同的nine-patches背景图。
Using the AppWidgetProvider Class
AppWidgetProvider类继承自BroadcastReceiver方便处理App Widget广播。AppWidgetProvider只接收与App Widget相关的广播事件。例如updated,deleted,enabled和disabled。当这些广播事件发生时,AppWidgetProvider接收下列回调。
  • onUpdate()——根据updatePeriodMillis属性定义的时间进行调用。同时当用户添加App Widget时也进行调用。所以在这里需要进行必要的设置,例如定义View视图的时间句柄同时如果必要的话启动一个temporary Service。然而如果定义了一个配置Activity,当用户添加App Widget时则不会需要调用这个方法。而配置Activity则有一无在第一次添加时完成配置。详见Creating an App Widget Configuration Activity.
  • onAppWidgetOptionsChanged()——用户第一次放置widget和其它任何时候改变widget尺寸的时候调用。可以使用这个回调根据widget的边界范围来显示和隐藏内容。调用getAppWidgetOptions()获取尺寸边界。它返回一个Bundle对象包括下述内容:
  1. OPTION_APPWIDGET_MIN_WIDTH——当前宽度的下边界
  2. OPTION_APPWIDGET_MIN_HEIGHT
  3. OPTION_APPWIDGET_MAX_WIDTH
  4. OPTION_APPWIDGET_MAX_HEIGHT——当前宽度的上边界
  • 这个回调在API Level16引入(Android4.1)更低版本的机器不能使用
  • onDeleted(Context, int[])——App Widget从App Widget host中删除时调用这个回调。
  • onEnabled(Context)——当一个App Widget实例第一次添加时调用。例如,用户添加了两个App Widget实例,这个回调只在第一次添加时调用。如果你想打开一个新的database或者其它所有App Widget都会使用的设置操作,那么这里是比较好的地方。
  • onDisabled(Context)——最后一个App Widget实例被删除时调用。这里是清楚onEnabled(Context)所有操作的地方,例如删除一个临时database。
  • onReceive(Context, Intent)——所有广播发生时调用,并且发生于上述所有回调方法之前。通常不需要实现这个方法因为缺省AppWidgetProvider实现过滤了所有App Widget广播并且只在合适的时候调用上述方法。
最红要的AppWidgetProvider回调是onUpdate()。因为每个App Widget添加到host时都会调用这个方法(有配置activity时除外)。如果App Widget接收任何用户交互事件,则需要在这个回调中注册事件句柄。如果App Widget不需要创建任何临时文件夹或databases,或作其它操作需要被清理。那么onUpdate()是我们唯一需要定义的回调。例如,下述App Widget有一个按钮启动一个Activity,AppWidgetProvider实现如下:
public class ExampleAppWidgetProvider extends AppWidgetProvider{
         public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds){
                    final int N = appWidgetIds.length;
                    for(int i = 0; i < N; i++){
                         int appWidgetId = appWidgetIds[i];
                         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.updateAppWidget(appWidgetId, views);
                    }
          }
}
这个AppWidgetProvider定义了onUpdate()方法,其中定义了PendingIntent启动一个Activity,并且根据setOnClickPendingIntent(int, PendingIntent)附着于App Widget按钮。注意到它包括一个循环。这个循环处理所有添加AppWidget的ID操作。这样,用户添加了多个App Widget实例同时进行更新。例如一个app widget两小时更新一次。而用户在一个小时之后添加了另外一个实例,那么这两个实例都会在第一个实例的两小时之后更新,而不再是第二个实例的两小时之后。
Note:AppWidgetProvider继承自BroadcastReceiver,那么回调返回之后并不能保证程序的继续运行(详见BroadcastReceiver的生命周期)如果App Widget的设置过程需要花费一定的时间(可能是网络操作)而我们需要程序继续运行,那么就要考虑在onUpdate()回调中启动一个Service。在Service中进行更新操作而不需要担心AppWidgetProvider因ANR错误而关闭。查看Wiktionary sample's AppWidgetProvider运行Service的App Widget实例。同时可见ExampleAppWidgetProvider.java样例类。
Receiving App Widget broadcast Intents
AppWidgetProvider只是一个方便的类。如果想直接接收App Widget广播,可以实现自己的BroadcastReceiver和复写onReceive(Context, Intent)回调。需要关注的Intents如下:
  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED
Creating an App Widget Configuration Activity
如果用户在添加一个App Widget时需要配置设置,那么可以创建一个app widget configuration Activity。这个Activity由App Widget host自动启动,并且允许用户在创建的时候配置可能的设置,例如App Widget color,size,更新周期和其它设置。
configuration Activity需要向普通activity那样在Android manifest文件中进行声明。然而它会被App Widget host中的ACTION_APPWIDGET_CONFIGUREaction启动。所以这个activity必须能接收这样的Intent。例如:
<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

同时,这个Activity必须在AppWidgetProviderInfo XML文件中声明,相应属性是android:configure。例如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.android.ExampleAppWidgetConfigure" 
    ... >
</appwidget-provider>

注意到Activity声明使用的是完全名空间。因为它可以在相应的包外指定。
先在我们需要定义一个Activity。实现这个Activity需要注意两件事:
  • App Widget host调用配置Activity,配置Activity需要总是返回一个结果,结果里需要包含App Widget ID,它由Intent传递启动配置Activity(作为EXTRA_APPWIDGET_ID保存在Intent extras中)。
  • 创建App Widget时将不再调用onUpdate()方法(系统在启动配置Activity时将不会发送ACTION_APPWIDGET_UPDATE广播)那么App Widget第一次创建时,配置Activity需要向AppWidgetManager要求更新。然而onUpdate()在后续更新中将会被调用——即只在第一次创建是跳过它。
Updating the App Widget from the configuration Activity
当App Widget使用配置Activity时。配置完成时更新App Widget的责任就交给了这个Activity。它的做法是向AppWidgetManager要求更新。
下面是App Widget在合适的时候更新和关闭配置Activity的过程:
  • 首先,从启动Activity的Intent中获取App Widget ID。
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId = extras.getInt(
            AppWidgetManager.EXTRA_APPWIDGET_ID, 
            AppWidgetManager.INVALID_APPWIDGET_ID);
}

  • 进行App Widget配置。
  • 配置完成时,调用getInstance(Context)获取AppWidgetManager实例。
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

  • 调用updateAppWidget(int, RemoteViews)获取RemoteViews layout更新App Widget。
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

  • 最后,创建返回Intent,设置给Activity返回结果,并且结束Activity。
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

Tip:配置Activity第一次打开时,设置Activity result为RESULT_CANCLED。这样,用户在未完成配置设置而退出时,App Widget被告知配置取消,将不会添加App Widget。
查看ApiDemos中的ExampleAppWidgetConfigure.java实例类。
Setting a Preview Image
Android 3.0引入了previewImage域。它指定了app widget的预览视图。这个preview在小部件选择器中展示给用户。如果未提供这个域,将使用app widget icon。如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  android:previewImage="@drawable/preview">
</appwidget-provider>

Android emulator包含了一个应用程序叫做"Widget Preview"。创建一个preview image,启动这个应用程序,选择一个app widget设置preview image出现的样式,最后放置到应用程序的drawable资源中。
Enabling App Widgets on the Lockscreen
Android4.2允许用户把widget添加到锁屏界面。声明android:widgetCategory属性指定AppWidgetProviderInfo表明这个app widget支持锁屏界面。这个属性值为"home_screen"和"keyguard"。如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   ...
   android:widgetCategory="keyguard|home_screen">
</appwidget-provider>

如果声明的一个widget既可以放置在主屏上,也可以放置在keyguard上,很可能我们希望根据放置的位置定制不同的widget。例如,创建不同的layout文件。那么下一步就是在运行时检测widget category进行响应。调用getAppWidgetOptions()获取widget放置位置的Bundle数据。返回的Bundle将会包括key值OPTION_APPWIDGET_HOST_CATEGORY,它的值为WIDGET_CATEGORY_HOME_SCREEN或者WIDGET_CATEGORY_KEYGUARD。然后在AppWidgetProvider中检测widget category。例如:
AppWidgetManager appWidgetManager;
int widgetId;
Bundle myOptions = appWidgetManager.getAppWidgetOptions(widgetId);
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1);
boolean isKeyguard = category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
一旦知道了widget的category,就可以有选择的装载baselayout,设置不同的属性等。例如
int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout :R.layout_widget_layout;

也可以通过android:initialKeyguardLayout属性指定app widget在锁屏界面的初始化layout。它同android:initialLayout工作方式相同。
Using App Widgets with Collections
Android3.0引入了有容器的app widgets。这类App Widget使用RemoteViewsService展示remote data备份的容器例如content provider。RemoteViewsService提供的数据显示在app widget中使用下述view类型,我们指容器视图。
ListView,GridView,StackView,AdapterViewFlipper
如上所述,这些容器视图展示远程数据备份的容器。这意味着它们使用一个Adapter帮定它们的数据到接口。一个Adapter帮定set数据个人选项到个人View对象。因为这些容器视图由adapters备份,那么Android framework必须提供另外的结构来支持它们在app widgets中的应用。在一个app widget的context中,Adapter被RemoteViewsFactory替代,它只是adapter接口的简约包装。当容器中要求一个特定选项时,RemoteViewsFactory 创建并返回一个选项作为容器的RemoteViews对象。在app widget中包括容器视图,必须实现RemoteViewsService和RemoteViewsFactory。
RemoteViewsService是允许一个remote adapter要求RemoteViews对象的service。
RemoteViewsFactory是一个容器视图(例如ListView, GridView等)和提供给它们的相应的数据的适配接口。在StackView Widget sample中,有如下公式化的实现service和接口的代码:
public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

//... include adapter-like methods here. See the StackView Widget sample.

}

Sample application
下面部分的代码来源于StackView Widget sample:

这个样例由10个View栈组成,依次显示值“0!”到“9!”,这个样例app widget有如下基础行为:
  • 用户可以纵向滑出顶部的View显示下一个或先前的View。这是一个built-in StackView行为。
  • 没有用户交互行为时,app widget自动有序的向前显示Views。这依赖于res/xml/stackwidgetinfo.xml文件中android:autoAdvanceViewId="@id/stack_view"的设置。这个设置应用于view ID,本例中是stack view的ID。
  • 用户触摸顶层的view时,app widget显示Toast信息“Touched view n”,更多相关信息,查看Adding behavior to individual items。
Implementing app widgets with collections
使用容器实现app widget,必须依照下述步骤。
  • Manifest for app widgets with collections
除了Declaring an app widget in the Manifest部分列出的相关要求外,为了使有容器的app widget帮定到RemoteViewsService中去,必须在manifest中申明BIND_REMOTEVIEWS权限。这一权限阻止了其它应用程序自由的获取app widget的数据。例如:
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />

android:name="MyWidgetService"指我们继承RemoteViewsService的相关子类。
  • Layout for app widgets with collections
此类app widget layout XML文件的唯一要求是包含下述容器视图之一:ListView, GridView,StackView或AdapterViewFlipper。如下是StackView Widget sample的widget_layout.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显示的是StackView为空时的状态。
除了整个app widget的layout file之外,我们还需要创建另外的定义了layout中每个item的layout file。例如,StackView Widget sample只有一个layout file——widget_item.xml,因为所有的item使用了相同的layout。但是WeatherListWidget sample有两组layout文件:dark_widget_item.xml和light_widget_item.xml。
  • AppWidgetProvider class for app widgets with collections
通常编写一个app widget的大部分代码都在AppWidgetProvider的子类的onUpdate()函数里,带有容器的app widget的主要不同点是在onUpdate()函数中必须调用setRemoteAdapter()函数。这个函数告知容器从何处获取数据。然后RemoteViewsService会返回RemoteViewsFactory的实现,widget会提供合适的数据。当调用这个方法的时候,必须传递一个指向RemoteViewsService的实现的intent和app widget的ID指定更新哪一个。
例如:
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds){
           for(int i = 0; i < appWidgetIds.length; ++i){
                     Intent intent = new Intent(context, StackWidgetService.class);
                      intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
                      RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
                      rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
                      rv.setEmptyView(R.id.stack_view, R.id.empty_view);
                      appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
           }
                      super.onUpdate(context, appWidgetManager, appWidgetIds);
}

  • RemoteViewsService class

如上所述,RemoteViewsService子类提供RemoteViewsFactory用来放置远程容器视图。特别的,需要做以下两步:

  • RemoteViewsService子类是远程adapter要求RemoteViews的地方。
  • 在RemoteViewsService子类中,包含一个实现了RemoteViewsFactory接口的类。RemoteViewsFactory是为了适配远程容器视图(如ListView, GridView)和提供给该视图的数据所设计的接口。那么这种实现就必须为data set中的每项提供RemoteViews对象。这个接口是Adapter的简单封装。
  • RemoteViewsFactory interface
实现RemoteViewsFactory接口的自定义类提供了容器中带有数据项的app widget。这样做的方法是把app widget 没项的XML文件同一组数据资源结合起来。这组数据资源可以是database或simple array中的任何数据。在StackView Widget sample中,这组数据资源是一组widgetItems。RemoteViewsFactory的作用即是把数据和容器视图适配起来。
RemoteViewsFactory中的onCreate()和getViewAt()函数必须实现。系统在第一次创建这个工厂类时调用onCreate()。同时这里是设置连接或cursor到数据资源的地方。例如:
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(){
                      for(int i = 0; i < mCount; i++){
                            mWidgetItems.add(new WidgetItem(i + "!"));
                       }
                       ...
            }
            ...
}

public RemoteViews getViewAt(int position){
             RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
             rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
             ...
             return rv;
}

Adding behavior to indivual items
上面部分展示了绑定数据到app widget collection的方法。如果我们需要为容器视图中的单个选项添加动态行为应当如何处理呢?
如Using the AppWidgetProviderClass部分描述的那样,通常我们使用setOnClickPendingIntent()设置对象的点击行为——例如点击启动一个Activity。点击容器中的单个选项我们使用setOnClickFillInIntent(),它导致为容器视图设置一个pending intent模板,然后通过RemoteViewsFactory为每个单独项设置填充intent。
下面的StackView Widget sample样例描述了如何为单独项添加行为。当我们点击顶层视图时显示Toast message“Touched view n”。如下:
  • StackWidgetProvider(AppWidgetProvider子类)创建一个pending intent拥有自定义action——TOAST_ACTION。
  • 用户触摸视图时,触发该intent并广播TOAST_ACTION。
  • 这个广播由StackWidgetProvider的onReceive()函数拦截,由app widget显示这个Toast信息。数据则由RemoteViewsFactory通过RemoteViewsService提供。
Note:StackView Widget样例使用了广播,通常一个典型的app widget会简单的启动一个activity。
Setting up the pending intent template
StackWidgetProvider设置了一个pending intent。容器中的单独项没有他们自己的pending intents。取而代之的是容器作为一个整体设置一个pending intent 模板,容器中的单独项在item-by-item基础上设置填充intent创建独立的行为。
这个类同时会接收用户点击视图时发送的广播。它在onReceive()方法中处理这个事件。如果intent的action是TOAST_ACTION,则显示那个Toast信息。
public class StackWidgetProvider extends AppWidgetProvider{
           public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
           public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";
 @Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
            int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
        }
        super.onReceive(context, intent);
    }
    
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

        for (int i = 0; i < appWidgetIds.length; ++i) {
    

            Intent intent = new Intent(context, StackWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);

            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

            rv.setEmptyView(R.id.stack_view, R.id.empty_view);

            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            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);
            
            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        }
    super.onUpdate(context, appWidgetManager, appWidgetIds);
    }

}
Setting the fill-in Intent
RemoteViewsFactory必须为容器中的每个选项设置填充intent。这使得区分一个选项的单独点击事件成为可能。填充的intent然后与PendingIntent模板相结合以决定点击项目后最终需要处理的事件。
public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

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);
    }

    // Initialize the data set.
        public void onCreate() {
            // In onCreate() you set up 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 + "!"));
            }
           ...
        }
        ...
    
        // Given the position (index) of a WidgetItem in the array, use the item's text value in 
        // combination with the app widget item XML file to construct a RemoteViews object.
        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;
        }
    ...
    }

Keeping Collection Data Fresh
下图描述了更新时使用容器的app widget的时序图,它展示了app widget部分代码是如何与RemoteViewsFactory进行交互和触发更新的:

使用容器的app widget的一个特征是提供给用户即时更新的内容。例如,考虑安卓3.0Gmail app widget,提供了用户对收件箱截屏的功能。实现这样的功能,我们需要触发RemoteViewsFactory和容器视图获取和展示新数据。通过AppWidgetManager调用notifyAppWidgetViewDataChanged()函数获取它。这同时会回调RemoteViewsFactory的onDataSetChanged()方法,获取新数据。注意到在onDataSetChanged()回调中可以同步processing-intensive操作。我们必须保证这一操作必须在从RemoteViewsFactory中获取metadata和视图数据的操作之前完成。除此之外,还可以在getViewAt()方法中进行processing-intensive操作。如果这个回调占用时间较长,加载中的视图(由RemoteViewsFactory的getLoadingView()方法指定)将会在容器视图的响应位置展示知道返回。
原文地址:https://www.cnblogs.com/dyllove98/p/3119986.html