Widget我爱编程

App Widgets

2018-02-09  本文已影响59人  Frank_Kivi

app Widgets是可以包含在其它app的小型app图标(比如桌面),它可以定期更新。这些view在用户界面上就是Widgets.我们可以使用App Widget provider来发布一个. 可以包含其它app widgets的组件叫做App Widget host。下面的截图展示了Music App Widget.


本文讨论如何使用App Widget provider来创建一个App Widget. 参阅AppWidgetHost来了解如何创建一个自己的AppWidgetHost.

Widget设计

参阅Widgets design guide来了解如何设计app widget.

基本知识

创建一个app widget,我们需要以下:
1. AppWidgetProviderInfo object
描述App Widget的元数据,比如App Widget的layout,更新频率和AppWidgetProvider类。这个必须在XML中定义
2. AppWidgetProvider class implementation
  定义基本的方法来让我们和app widget根据广播事件进行程序化的交互。通过它,我们可以在App Widget更新,启用,禁用,或者删除时接收到广播,
3. View layout
定义了app widget的初始化layout,定义在XML中。

另外,我们可以实现一个App Widget configuration Activity.这是一个当用户添加我们的app widget时,可以让用户在创建时修改app widget设置启动的可选activity.

下边讨论如何创建这些组件.

在清单文件中声明一个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>

<receiver>节点需要android:name属性,它指定了app widget使用的AppWidgetProvider。

<intent-filter>节点需要包含一个<action>节点,这个<action>有一个android:name属性。这个属性指定了AppWidgetProvider接收ACTION_APPWIDGET_UPDATE的广播。这是唯一我们必须显示声明的广播。AppWidgetManager会在必要时自动发送所有的其它app widget 广播给AppWidgetProvider。

<meta-data>节点指定了AppWidgetProviderInfo的资源和需要的下列属性:
1. android:name 指定了元数据的名字。使用android.appwidget.provider来标记数据为AppWidgetProviderInfo descriptor。
2. android:resource 指定了AppWidgetProviderInfo的资源位置。

添加AppWidgetProviderInfo元数据

AppWidgetProviderInfo定义了App Widget的核心特性,比如最小的布局大小,初始化布局资源,更新频率和(可选)一个配置 activity在创建时启动。在XML中使用<appwidget-provider>节点定义 AppWidgetProviderInfo然后把它保存到项目的res/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>

下面是<appwidget-provider>的属性总结:

1.minWidth和minHeight指定了默认情况下app Widget占用的最小空间。Home Screen根据定义好的网格来放置app widget. 如果app widget的最小宽度和调试不匹配网格的大小,app widget会向上取整来靠近网格的大小。
参阅App Widget Design Guidelines
备注:为了让app widget可以在设备间移动,最小尺寸不能超过4*4个cell.

  1. minResizeWidth和minResizeWidth指定了app widget的绝对最小尺寸。低于这些尺寸,app widget会不可使用。使用这些属性可以让用户调用widget的大小可能比默认的大小小一些。Android 3.1引入。
    参阅App Widget Design Guidelines
    3.updatePeriodMillis定义了App Widget调用onUpdate()来向AppWidgetProvider请求一个更新的时间间隔。真正更新的时间不能保证恰好是这个时间,建议更新尽量不在太频繁,不要超过一个小时一次这样可以省电。也可以让用户来在配置中调整,有些用户可以想让一个stock ticker每15分钟更新一次,或者一天四次。
    备注:如果该更新时,设备在休眠,那么为了更新,设备会唤醒。如果我们的更新频率不高于一小时一次,那么应该不会对电量造成太大的影响。但是,如果我们需要更高的更新频率或者我们不想要在设备休眠的时候更新,那么我们可以根据一个不会唤醒设备的闹钟来更新。想要实现这个操作,使用AlarmManager来设置一个带有我们的AppWidgetProvider可以接收到的intent的闹钟。设置闹钟的类型为ELAPSED_REALTIME或者RTC,这样只有在设备清醒的时候才会起作用。然后设置updatePeriodMillis为0.
    4.initialLayout属性指向了一个定义App Widget layout的 layout 资源。
    5.configure属性定义了当用户添加App Widget时,为了配置App Widget的属性而启动的一个activity. 这个是可选的(参阅下边的Creating an App Widget Configuration Activity)。
    6.previewImage指定了一个app widget配置后的预览,用户在选择app widget时可以看到。如果没有提供,用户会看到我们app的启动图标。这个对应了清单文件中的<receiver>节点的android:previewImage。参阅Setting a Preview Image。Android 3.0引入。
    7.autoAdvanceViewId属性指定了一个app widget subview的id。它应该被widget的host自动置为高优先级。Android3.0引入。
    8.resizeMode属性指定了widget可以被重新设置大小的规则。使用这个属性来让homescreen的widget水平调整大小,竖直调整大小或者两者都进行。用户按着widget来处理它的调整大小模式,在网格中通过拖拽来实现水平或者竖直方向的调整。resizeMode的值包括水平,竖直或者none. 如果同时支持水平和竖直,使用 "horizontal|vertical".
    9.minResizeHeight属性指定了widget可以调整的最小高度。如果比minHeight大或者竖直调整没有开启,都是不起作用的。
    10.The minResizeWidth属性指定了widget可以调整的最小宽度。如果比minWidth大或者水平调整没有开启,都是不起作用的。
    11.widgetCategory属性声明了app widget是否可以在home screen,ock screen或者两个上展示。只有低于5.0的支持lock-screen widgets,5.0及以上版本,只有home_screen是有效的。

参阅AppWidgetProviderInfo

创建一个app widget layout

我们必须为app widget在xml中定义一个初始的layout,然后保存到project的res/layout/. 我们可以使用下列的View对象来设计自己的widget,但是在开始之前,请参阅App Widget Design Guidelines

如果我们熟悉layout,创建一个app widget比较简单. 我们必须清楚,app widget layout是基于remoteView的,它不支持所有的layout和view widget.

一个RemoteViews对象(最后是一个app widget)可以支持下列layout:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

下列widget:

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

这些类的子类是不支持的。

RemoteViews也支持ViewStub,它是不可见的,没有尺寸的view,我们可以用来在运行时懒加载。

给app widget添加margin

Widgets不应该扩展到屏幕边缘,不应该和其它的widget在视觉上有交集,所以我们应该在widget的每一边都添加margin.

Android4.0,app widget是自动添加了padding来和其它的widget进行对齐等。为了利用这个特点,设置app的targetSdkVersion为14或者更高。

写一个单一layout来支持低版本的平台添加margin,4.0及以上平台没有margin是非常容易的。

1.设置app的targetSdkVersion为14或者更高。
2.创建一个layout,使用引用的方式来设置margin,示例如下:

<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.创建两个dimen resource,一个位于res/values/来提供低于4.0平台,一个位于res/values-v14/来提供4.0平台的。

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

另外一个方法是使用不同的nine-patch做背景。

使用AppWidgetProvider类

我们必须在清单文件中声明自己的AppWidgetProvider实现,它是作为一个broadcast receiver,使用 <receiver>来声明的。

AppWidgetProvider继承BroadcastReceiver是一个方便的方式来处理app widget的广播。AppWidgetProvider只接收那些有关系到app widget的事件广播,比如app widget更新,删除,启用和禁用。当这些广播事件发生时,AppWidgetProvider会接收到下列方法的调用:
1.onUpdate()
在AppWidgetProviderInfo定义的updatePeriodMillis间隔时,这个方法会被调用来更新app widget.这个方法也会在用户添加一个app widget时调用,这样它就可以执行核心的初始化,比如为view定义事件处理和开启一个临时的服务。但是,如果已经声明了一个configuration activity,当用户添加一个app widget时,这个方法不会被调用,而是在后续更新的时候才调用。这时是configuration Activity在执行configuration完成后执行第一次更新。
2.onAppWidgetOptionsChanged()
这个方法会在第一次app widget放置或者任何调整大小的时候调用。我们可以使用这个回调来根据widget的大小来展示或者隐藏一些内容。可以调用getAppWidgetOptions()来得到widget的大小,它会返回一个bundle,包含如下信息:
1.OPTION_APPWIDGET_MIN_WIDTH—Contains the lower bound on the current width, in dp units, of a widget instance.
2.OPTION_APPWIDGET_MIN_HEIGHT—Contains the lower bound on the current height, in dp units, of a widget instance.
3.OPTION_APPWIDGET_MAX_WIDTH—Contains the upper bound on the current width, in dp units, of a widget instance.
4.OPTION_APPWIDGET_MAX_HEIGHT—Contains the upper bound on the current width, in dp units, of a widget instance.
这个回调是在API16之后才加入的,所以如果要实现这个回调,确保你没有依赖它,因为在老的设备上它不会调用。
3.onDeleted(Context, int[])
app wdiget从 App Widget host删除时调用。
4.onEnabled(Context)
当一个app widget的实例第一次被创建时会调用。比如,用户添加了两个app widget的实例,只有第一次创建时调用。如果我们需要打开一个新的database来执行对所有app widget实例只需要进行一次的初始化操作,这个方法是很好的。
5.onDisabled(Context)
最后一个app widget的实例被删除时会调用。我们应该进行一些在onEnable的清理工作,比如,删除一个临时的database.
6.onReceive(Context, Intent)
每个广播和上边的方法调用前都会调用这个方法。通常我们不需要实现这个方法,因为默认的AppWidgetProvider实现会过滤所有的app widget广播,然后调用上边的适当方法。

AppWidgetProvider最重要的回调是onUpdate,因为它会在每个app widget添加到host时调用(除非使用了configuration activity). 如果我们的app widget接收任何用户的交互事件,我们需要在这个方法中注册事件回调。如果我们不关心创建临时的文件和数据库,也不其它需要清理的工作,那么onUpdate也许是唯一我们需要实现的方法。比如,如果我们想让app widget有一个button,在点击时启动一个activity,可以使用下边的AppWidgetProvider实现:

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this provider
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];

            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            // Get the layout for the App Widget and attach an on-click listener
            // to the button
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

这个AppWidgetProvider只定义了onUpdate方法,用来定义一个启动activity的PendingIntent和使用setOnClickPendingIntent(int, PendingIntent)关联这PendingIntent到app widget的button上.注意它包含了一个循环,循环会遍历appWidgetIds的每一个入口,appWidgetIds是provider创建的一个用来标记widget的id数组。这样,如果用户创建了多个widget对象,他们都会自动更新。但是只会安排一个updatePeriodMillis。比如,如果更新的安排是每两个小时,第二个widget实例是在第一个添加之后 的1小时后添加的,那么他们都会在第一个定义的时间间隔更新,第二个的会被忽略(他们两个都是两小时更新一次,不是1小时一次)

备注:因为AppWidgetProvider是BroadcastReceiver的扩展,我们的进程不能保证在这些回调方法返回后还能一直运行。如果app widget的setup process可以持续几秒种(可能是在请求网络),我们需要继续下去,考虑在onUpdate方法中启动一个Service。在这个Service中,我们可以更新自己的app widget,不需要担心AppWidgetProvider因为ANR停掉了。参阅Wiktionary sample's AppWidgetProvider。

也可以参阅 ExampleAppWidgetProvider.java

接收app widget的广播intent

AppWidgetProvider只是一个辅助类。如果我们想要直接接收app widget的广播,我们可以实现自己的BroadcastReceiver或者重写onReceive(Context, Intent)。我们需要关心的intent是以下几种:

ACTION_APPWIDGET_UPDATE
ACTION_APPWIDGET_DELETED
ACTION_APPWIDGET_ENABLED
ACTION_APPWIDGET_DISABLED
ACTION_APPWIDGET_OPTIONS_CHANGED

创建一个app widget configuration activity

如果想让用户在添加一个新的app widget时自己去配置,我们可以创建一个app widget configuration activity. 这个activity 会被app widget host自动启动,让用户在app widget创建时来做可能的配置,比如app widget的color,size,更新频率和其它功能性设置。

configuration activity应该作为一个普通的activity在清单文件中声明。但是,它会被app widget host通过ACTION_APPWIDGET_CONFIGURE action来启动,所以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是使用全类名声明的,因为它会在我们的包范围之外引用。

这就是所有我们需要了解的有关configuration activity的准备工作。现在我们需要一个真正的activity。但是,在实现activity时需要牢记两点:

1.app widget host调用configuration Activity,configuration Activity需要返回一个结果。这个结果应该包含传入intent中的app widget的id(在intent的EXTRA_APPWIDGET_ID中).
2.app widget创建时onUpdate方法不会被调用(当有一个configuration activity启动时,系统不会发送ACTION_APPWIDGET_UPDATE的广播)。所以configuration activity需要在第一次创建时请求更新。但是 onUpdate()在后续还会调用,只是跳过了第一次。

参阅下边的代码

从configuration activity中更新app widget

当app widget使用configuration activity时,activity需要在配置完成后去更新app widget. 我们可以通过向AppWidgetManager直接请求更新来实现。

下边是更新app widget并且关闭configuration activity的过程总结:

1.首先,从启动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);
}
  1. 执行app widget的配置
  2. 当配置完成时,通过 getInstance(Context)来得到一个AppWidgetManager的实例:
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
  1. 调用updateAppWidget(int, RemoteViews)来通过一个RemoteViews的layout来更新app widget:
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

5.最后,创建一个返回的intent,设置为activity的result,然后返回activity:

Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

提示:当我们的configuration activity第一次打开时,设置activity的result是RESULT_CANCELED。这样,如果用户在到达末尾之前退出activity, app widget host 会被通知configuration被取消,app widget就不会添加。

参阅 ExampleAppWidgetConfigure.java

Setting a Preview Image

android 3.0引入了previewImage字段,它指定了一个app widget的预览图。这个图会在widget picker中显示给用户。如果没有指定这个字段,app widget的icon就会用来做预览图。

示例如何在XML中指定这个设置:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  android:previewImage="@drawable/preview">
</appwidget-provider>

为了帮助创建一个preview image,Android emulator包含了一个叫做"widget preview"的app. 为了创建preview image, 启动这个app,选择一个app widget,设置我们想如何显示自己的preview image, 然后保存到我们app的resources中。

Using App Widgets with Collections

3.0引入了集合的app widgets. 这种app widgets使用RemoteViewsService来展示集合,数据是远程的,比如一个content provider. 这个由RemoteViewsService提供的数据在app widget中使用下边的view类型来展示,我们称为"collection views:"

ListView
GridView
StackView
AdapterViewFlipper

如前所述,这些collection views可以展示remote data支持的集合。这就意味着他们使用adapter来绑定数据和界面。一个adapter绑定集合中的一个item到一个view上。因为这些集合的view由adapter支持,所以android框架必须包含特殊的架构来支持app widget. 在使用app widget时,adapter被RemoteViewsFactory替代,这是一个adapter接口的简单封装。当请求一个具体的item时,RemoteViewsFactor会创建并返回一个item,作为一个RemoteViews对象。为了能包含一个collection view, 我们必须实现RemoteViewsService和RemoteViewsFactory。

RemoteViewsService是一个service,它允许一个remote adapter来请求RemoteViews。RemoteViewsFactory是一个adapter的接口,在collection view(比如ListView,GridView等)和它绑定的数据之间。下边是一个示例:

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.

}

示例APP

这部分的代码出自StackView Widget示例:


示例包含了10个view,展示从"0!"到"9!".示例的app有以下主要功能:

1.用户可以竖直滑动app widget的头部来切换到下一个或前一个view. 这是StackView的功能。
2.不需要用户交互,app widget可以通过自己的view序列自动前进,就像幻灯片一样。这是通过res/xml/stackwidgetinfo.xml的android:autoAdvanceViewId="@id/stack_view"来实现的。这个设置应用于view的id,也就是stack view的view ID.
3.如果用户触摸top view, app widget会展示toast"Touched view n"(n是角标)。参阅Adding behavior to individual items

使用collection实现app widget

为了使用collection实现app widget,我们应当遵循前边的基本步骤。下边讨论我们需要额外操作的步骤。

Manifest for app widgets with collections
除了Declaring an app widget in the Manifest中需要的之外,还需要声明BIND_REMOTEVIEWS的权限。这阻止其它app访问我们的数据。比如,当使用RemoteViewsService来填充collection view时,manifest entry可能是这样的:

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

android:name="MyWidgetService"是我们RemoteViewsService的实现类全称。

Layout for app widgets with collections
layout XML的主要需求是它要包含一个collection view:ListView, GridView, StackView, or AdapterViewFlipper. 下边是一个StackView Widget示例的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一定是collection view的兄弟节点。

除了定义app widget的layout file之外,我们还需要创建另外一个layout file定义每个item(比如,在books collections中的一个book的layout). 比如,StackView Widget只有一个layout file,widget_item.xml,因为所有的item使用同一个layout. 但是WeatherListWidget有两个layout files:dark_widget_item.xml and light_widget_item.xml.

AppWidgetProvider class for app widgets with collections
对于正常的app widget,我们的代码都在onUpdate()中。使用collection的app widget主要区别在于需要在onUpdate()中调用setRemoteAdapter()。这是用来通知collection view去哪取数据。RemoteViewsService然后可以返回我们实现的RemoteViewsFactory,然后widget就可以填充适当的数据。当我们调用这个方法时,必须传递指向我们实现的RemoteViewsService的Intent和app widget id.

比如,下边是StackView Widget的onUpdate()方法来设置RemoteViewsService作为app widget collection的 remote adapter:

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 class

Persisting data
我们不能依赖一个service实例或者它包含的数据来持久化。我们不应该在自己的RemoteViewsService中存储数据(除非它是static).如果我们想让数据持久化,最好的方法是使用ContentProvider,provider的数据是持久化的。

使用RemoteViewsService的子类提供一个RemoteViewsFactory来填充remote collection view。

具体就是说,需要执行以下步骤:

1.继承RemoteViewsService.RemoteViewsService是一个service,通过它remote adapter可以请求RemoteViews。
2.在我们的RemoteViewsService子类中包含一个实现RemoteViewsFactory的子类。RemoteViewsFactory是一个adapter接口,在remote collection view(比如ListView, GridView等)和data之间。我们的实现有责任来给每个item设置一个RemoteView.接口是Adapter的简单封装。

RemoteViewsService的主要实现内容是RemoteViewsFactory,如下:

RemoteViewsFactory interface

我们实现RemoteViewsFactory来提供app widget的数据。为了这么做,它组合了app widget的item xml。数据源可能是一个database或者一个array. 在StackView Widget中,数据源是WidgetItems的数组。RemoteViewsFactory 作为一个adapter来把数据连接到remote collection view.

RemoteViewsFactory两个最重要的方法是onCreate() and getViewAt()

系统在第一次创建我们要的factory时会调用onCreate()。在这里我们设置connection,cursor到我们的数据源。比如,StackView Widget在 onCreate() 中初始化了一个WidgetItem数组。当app widget是活跃的,系统就会使用角标来访问这个数组展示它们的文本。

下面是StackView Widget 的RemoteViewsFactory的onCreate()部分代码:

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 + "!"));
        }
        ...
    }
...

RemoteViewsFactory的getViewAt()返回一个RemoteViews对应固定位置的数据。下边是RemoteViewsFactory的实现:

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

Adding behavior to individual items

上边展示了如何绑定数据到app widget collection. 但是如果我们想要向每个item添加一个动态的行为怎么实现呢?

Using the AppWidgetProvider已经讨论过了,我们通常使用setOnClickPendingIntent来设置一个对象的click,比如让一个button启动一个activity. 但是这种方法是不能设置在一个collection item中的子view的(我们可以使用setOnClickPendingIntent()来给Gmail app widget设置一个全局button,它可以启动一个activity,但是在list的每个item上不能设置)。为了给item设置点击事件,我们可以使用setOnClickFillInIntent(). 这个可以为collection view发送一个pending intent,然后通过RemoteViewsFactory设置一个intent在每个item上。

这部分使用StackView Widget来描述如何给item添加事件。在StackView Widget中,如果用户点击top view,app widget会展示Toast message "Touched view n,"n是角标。过程如下:

1. StackWidgetProvider创建一个带有TOAST_ACTION的pending intent。
2. 当用户点击一个view时,intent发送,然后广播TOAST_ACTION。
3. 这个广播被StackWidgetProvider的onReceive()方法拦截,然后app widget展示Toast message. 数据是由RemoteViewsFactory通过RemoteViewsService提供。

备注:StackView Widget使用一个broadcast, 但是app widget通常简单启动一个activity。

Setting up the pending intent template
StackWidgetProvider设置一个pending intent。 每个item不能设置自己的pending intents。collection整体设置了一个pending intent template,每个item设置一个fill-in intent来创建一个唯一的事件。

这个类也接收当用户点击view时发送的广播。它会在onReceive()中处理这个事件。如果Intent的action是TOAST_ACTION,app widget会展示一个Toast message。

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

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
    // displays a Toast message for the current 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) {
        // update each of the app widgets with the remote adapter
        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Sets up the intent that points to the StackViewService that will
            // provide the views for this collection.
            Intent intent = new Intent(context, StackWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            // When intents are compared, the extras are ignored, so we need to embed the extras
            // into the data so that the extras will not be ignored.
            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);

            // The empty view is displayed when the collection has no items. It should be a sibling
            // of the collection view.
            rv.setEmptyView(R.id.stack_view, R.id.empty_view);

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

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

Setting the fill-in Intent
我们的RemoteViewsFactory必须给每个item设置一个fill-in intent。这个可以区分指定item的点击事件。这个intent会和PendingIntent template绑定然后在item点击时最终决定发送intent.

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的代码是如何跟RemoteViewsFactory交互的,和我们应该如何触发更新:


使用collection的app widget的一个特点是可以给用户提供最新的content. 比如,Android 3.0的Gmail app widget,它给用户提供了一个收件箱的快照。为了实现这个功能,我们需要触发我们的RemoteViewsFactory和collection view去获取和展示最新数据。我们通过AppWidgetManager调用notifyAppWidgetViewDataChanged()来实现。这个调用会在RemoteViewsFactory’s中有一个onDataSetChanged()的回调,这就给我们机会去获取新数据。注意我们可以在这个方法中同步执行一些耗时操作。这个方法一定会在metadata或view data从RemoteViewsFactory返回前完成调用。另外,我们可以在getViewAt()中执行耗时操作。如果这个方法调用时间过长,loading view(RemoteViewsFactory’s的getLoadingView())会展示一起到它返回为止。

上一篇下一篇

猜你喜欢

热点阅读