Skip to content

Latest commit

 

History

History
765 lines (543 loc) · 48.4 KB

构建应用程序小部件.md

File metadata and controls

765 lines (543 loc) · 48.4 KB

构建应用程序小部件

原文(英文)地址

App Widgets是迷你应用程序视图,可以嵌入到其他应用程序(例如主屏幕)中并接收定期更新。这些视图在用户界面中称为窗口小部件,您可以使用 App Widget provider发布一个窗口小部件。能够容纳其他App Widgets的应用程序组件称为App Widget host。下图显示了音乐应用小部件:

appwidget

本文描述如何使用App Widget provider发布一个应用程序小部件。想了解如何创建你自己的AppWidgetHost以托管应用程序小部件,请参阅构建应用程序小部件主机(Host).

注意:关于如何设计你的app小部件的内容请参阅 App小部件概述

基础

要创建App Widget,您需要以下内容:

  • AppWidgetProviderInfo对象 描述App Widget的元数据,例如App Widget的布局,更新频率和AppWidgetProvider类。这应该在XML中定义。
  • AppWidgetProvider类的实现 定义允许您基于广播事件以使用代码与App Widget进行交互的基本方法。通过它,您将在App Widget更新,启用,禁用和删除时收到广播。
  • View layout 在XML中定义App Widget的初始布局。

此外,您可以实现配置App Widget的Activity。这是一个可选的Activity,当用户添加App Widget并允许他们在创建时修改App Widget设置时启动。

以下部分描述了如何设置每个组件。

在Manifest文件中声明App Widgets

首先在Manifest文件中声明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小部件使用的AppWidgetProvider。

<intent-filter>元素必须包含一个含有android:name属性的<action>元素,该属性(android:name)指定了AppWidgetProvider接受的ACTION_APPWIDGET_UPDATE广播。

这是你唯一需要显式声明的广播。AppWidgetManager会在必要的时候自动将所有其他App 小部件的广播发送到AppWidgetProvider。

<meta-data>元素指定了AppWidgetProviderInfo的resource,并且该元素需要以下属性:

  • android:name :指定metadata的名字。使用android.appwidget.provider将数据标识为AppWidgetProviderInfo描述符。
  • android:resource:指定了AppWidgetProviderInfo的资源位置

添加AppWidgetProviderInfo元数据(Metadata)

AppWidgetProviderInfo定义App Widget的基本属性,例如其最小布局维度,初始布局资源,更新App Widget的频率,以及(可选)在创建时启动的配置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: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>的属性总结:

  • minWidthminHeight属性的值指定App Widget默认使用的最小尺寸。默认主屏幕根据具有已定义高度和宽度的单元格网格在其窗口中定位App Widgets。如果App Widget的最小宽度或高度的值与单元格的尺寸不匹配,则App Widget尺寸将向上舍入到最接近的单元格大小。

    关于App Widgets布局的更多信息参阅App Widget Design Guidelines

  • updatePeriodMillis属性定义App Widget框架通过调用onUpdate()回调方法从AppWidgetProvider请求更新的频率。实际的更新不能保证准确地按时发生,我们建议尽可能不经常更新——也许每小时不超过一次以节省电量。您可能还允许用户调整配置中的频率——有些人可能希望股票代码每15分钟更新一次,或者一天只能更新四次。

    注意:如果设备在更新(由updatePeriodMillis定义)时处于睡眠状态,则设备将被唤醒以执行更新。如果每小时更新不超过一次,这可能不会导致电池寿命出现严重问题。但是,如果您需要更频繁地更新或者在设备处于睡眠状态时不需要更新,则可以基于不会唤醒设备的警报(alarm)执行更新。为此,请使用AlarmManager和AppWidgetProvider接收的Intent设置警报。将警报类型设置为ELAPSED_REALTIME或RTC,它仅在设备唤醒时发出警报。然后将updatePeriodMillis设置为零(“0”)。

  • initialLayout属性定义了App小部件的布局资源文件的位置

  • configure属性定义了当用户添加小部件到屏幕上时需要启动的Activity,以便用户配置App Wdiget,这是可选的,请参阅下面的创建一个配置App小部件的Activity

  • previewImage属性指定应用程序窗口小部件配置只后的外观预览,用户在选择应用程序窗口小部件时会看到该窗口小部件。如果未提供,则用户会看到应用程序的启动器图标。此字段对应于AndroidManifest.xml文件中<receiver>元素中的android:previewImage属性。有关使用previewImage的更多内容,请参阅设置预览图像

  • autoAdvanceViewId属性指定应由窗口小部件主机(host)自动提升(auto-advanced)的应用窗口小部件子视图的视图ID。在Android 3.0中推出。

  • resizeMode属性指定可以调整窗口小部件的规则。您可以使用此属性使主屏幕小部件可以水平,垂直或在两个轴上进行调整。用户触摸按住窗口小部件以显示其调整大小手柄,然后拖动水平和/或垂直手柄以更改布局网格上的大小。 resizeMode属性的值包括“horizontal”,“vertical”和“none”。要将窗口小部件声明为水平和垂直可调整大小,请提供值“horizontal | vertical”。在Android 3.1中引入。

  • minResizeHeight属性指定可以调整窗口小部件的最小高度(以dps为单位)。如果此字段大于minHeight或未启用垂直调整大小,则此字段无效(请参阅resizeMode)。在Android 4.0中推出。

  • minResizeWidth属性指定可以调整窗口小部件的最小宽度(以dps为单位)。如果此字段大于minWidth或未启用水平调整大小,则此字段无效(请参阅resizeMode)。在Android 4.0中推出。

  • widgetCategory属性声明您的App Widget是否可以显示在主屏幕(home_screen),锁屏(键盘保护)或两者上。只有低于5.0的Android版本才支持锁屏小部件。对于Android 5.0及更高版本,只有home_screen有效。

关于更多<appwidget-provider>元素的属性值,请参阅AppWidgetProviderInfo

创建应用程序小部件布局

你必须为你的app小部件定义一个舒适的布局xml文件,并且将其保存在res/layout目录下。你可以使用以下列出的View对象来设计你的App小部件,但是在你开始设计你的app小部件的布局之前,请先阅读app小部件设计原则.

如果你对Layouts很熟悉,那么构建一个app小部件的布局对你来说很容易。但是你必须注意,App小部件的布局是基于RemoteViews的,该组件不支持所有的View组件。

一个RemoteViews对象(以及App小部件)可以支持一下布局类:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

以及以下组件类:

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

这些类之外的其他类是不被支持的。

RemoteViews同样支持ViewStub,这是一个不可见的,零大小的视图,您可以使用它在运行时惰加载过大的布局资源。

在App小部件中添加边距(margins)

窗口小部件通常不应扩展到屏幕边缘,并且不应在视觉上与其他窗口小部件齐平,因此您应在窗口小部件框架的所有边上添加边距。

从Android 4.0开始,应用程序小部件会在窗口小部件框架和应用程序窗口小部件的边界框之间自动填充,以便更好地与用户主屏幕上的其他窗口小部件和图标对齐。要利用此强烈建议的行为,请将应用程序的targetSdkVersion设置为14或更高。

编写具有应用于早期版本平台的自定义边距,并且没有针对Android 4.0及更高版本的额外边距的单个布局很容易:

  • 将应用程序的targetSdkVersion设置为14或更高。

  • 创建一个如下所示的布局,引用其边距的 dimension resource

    <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>
  • 创建两个dimensions resources,一个在res/values/下,用来提供Android4.0之前的自定义边距(margins),另一个在res/values-v14,用于提供android 4.0之后的额外填充(padding)

    res/values/dimens.xml:

    <dimen name="widget_margin">8dp</dimen>

    res/values-v14/dimens.xml:

    <dimen name="widget_margin">0dp</dimen>

另一个选择是默认在nine-patch背景中添加额外的边距,并为API级别14或更高版本提供没有边距的nine-patch资源。

使用AppWidgetProvider类

AppWidgetProvider类继承自BroadCastReceiver,AppWidgetProvider类将BroadcastReceiver扩展为一个更加方便的类来处理App Widget广播。 AppWidgetProvider仅接收与App Widget相关的广播事件,例如更新,删除,启用和禁用App Widget。当这些广播事件发生时,AppWidgetProvider会收到以下方法调用:

  • onUpdate()

    该方法按AppWidgetProviderInfo中的updatePeriodMillis属性定义的间隔事件被调用,用来更新App Widget(请参阅上面的添加AppWidgetProviderInfo元数据)。用户添加App Widget时也会调用此方法,因此它应执行基本设置,例如为视图定义事件处理程序并在必要时启动临时服务。但是,如果已声明配置Activity,则在用户添加App Widget时不会调用此方法,但会为后续更新调用此方法。配置Activity负责在配置完成时执行第一次更新。 (请参阅下面的“创建一个配置App小部件的Activity”。)

  • onAppWidgetOptionsChanged()

    该方法在App小部件第一次被放置以及每次resize 小部件时会会被调用。你可以在本方法中根据小部件的尺寸隐藏或者增加一些内容。你可以使用getAppWidgetOptions()方法得到尺寸的范围,该方法返回一个Bundle类型的数据,该数据中包含了以下内容:

    本方法是在API 16(Android 4.1)中引入的。如果您实现了次回调,请确保在您的应用不会依赖于这个方法,因为该方法在老版本的设备上会失效。

  • onDeleted(Context, int[])

    小部件每次从小部件主机(host)中被删除时都会调用这个方法。

  • onEnabled(Context)

    当App小部件第一次被创建时会调用此方法。比如,用户添加了两个你app小部件的实例,该方法仅仅会在第一个小部件被创建时调用。如果您需要打开一个新数据库或执行其他只需要为所有App Widget实例发生一次的设置,那么这是一个很好的执行这些操作的地方。

  • onDisabled(Context)

    该方法会在最后一个实例被从App小部件主机删除的时候调用。在这里你应该清除任何在onEnabled(Context)中做的工作,比如删除临时的数据库。

  • onReceive(Context, Intent)

    在每个广播和每个上述回调方法被调用之前调用此方法。您通常不需要实现此方法,因为默认的AppWidgetProvider实现过滤所有App Widget广播并根据需要调用上述方法。

    注意:您必须使用AndroidManifest中的<receiver>元素将AppWidgetProvider类实现声明为广播接收器(请参阅上面的在Manifest中的声明应用程序小部件)。

AppWidgetProvider最重要的回调是onUpdate(),因为在将每个App Widget添加到主机时会调用它(除非您使用配置Activity)。如果您的App Widget接受任何用户交互事件,那么您需要在此回调中注册事件处理程序。如果您的App Widget不创建临时文件或数据库,或执行需要清理的其他工作,则onUpdate()可能是您需要定义的唯一回调方法。例如,如果您想要一个App Widget,其按钮在单击时启动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()方法,用于定义PendingIntent,该PendingIntent启动Activity并使用setOnClickPendingIntent(int,PendingIntent)将其附加到App Widget的按钮。请注意,它包含一个遍历appWidgetIds中每个item的循环,这是一个ID数组,用于标识此provider创建的每个App Widget。这样,如果用户创建了多个App Widget实例,那么它们都会同时更新。但是,将仅为App Widget的所有实例管理一个updatePeriodMillis。例如,如果更新计划(update schedule)定义为每两个小时,并且在第一个实例之后一小时添加App Widget的第二个实例,那么它们将在第一个小部件定义的时间段内更新,第二个小部件定义的更新将被忽略(它们将每两小时更新一次,而不是每小时更新一次)。

注意:因为AppWidgetProviderBroadcastReceiver的子类,所以在回调方法返回后,您的进程不能保证继续运行(有关广播生命周期的信息,请参阅BroadcastReceiver)。如果您的App Widget设置过程可能需要几秒钟(可能在执行Web请求时),并且您需要继续运行进程,请考虑在onUpdate()方法中启动Service。在Service中,您可以对App Widget执行自己的更新,而无需担心由于应用程序无响应(ANR)错误导致AppWidgetProvider关闭。有关运行服务的App Widget的示例,请参阅Wiktionary sample's AppWidgetProvider

同样请参考ExampleAppWidgetProvider.java示例代码。

接收App小部件的broadcast 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小部件

在Android8.0(API26)及以上,启动器不仅允许你创建pinned shortcuts(固定快捷方式),同样也支持你将App小部件固定在启动器上。和固定快捷方式类似,固定的App小部件给了用户访问你App中特殊任务的权限。

在你的App中,您可以通过完成以下一系列步骤来创建系统请求以将窗口小部件固定到支持的启动器上:

1:在Manifest文件中创建小部件,就像下面这样:

<manifest>
...
  <application>
    ...
    <receiver android:name="MyAppWidgetProvider">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
                   android:resource="@xml/my_appwidget_info" />
    </receiver>
  </application>
</manifest>

2:调用requestPinAppWidget()方法:

AppWidgetManager mAppWidgetManager =
        context.getSystemService(AppWidgetManager.class);
ComponentName myProvider =
        new ComponentName(context, MyAppWidgetProvider.class);

if (mAppWidgetManager.isRequestPinAppWidgetSupported()) {
    // Create the PendingIntent object only if your app needs to be notified
    // that the user allowed the widget to be pinned. Note that, if the pinning
    // operation fails, your app isn't notified.
    Intent pinnedWidgetCallbackIntent = new Intent( ... );

    // Configure the intent so that your app's broadcast receiver gets
    // the callback successfully. This callback receives the ID of the
    // newly-pinned widget (EXTRA_APPWIDGET_ID).
    PendingIntent successCallback = PendingIntent.createBroadcast(context, 0,
            pinnedWidgetCallbackIntent);

    mAppWidgetManager.requestPinAppWidget(myProvider, null, successCallback);
}

注意:如果您的应用程序不需要通知系统是否成功将小部件固定到支持的启动程序上,您可以将null作为第三个参数传递给requestPinAppWidget()。

创建一个配置App小部件的configuration Activity

如果您希望用户在添加新的应用程序窗口小部件时配置设置,则可以创建应用程序窗口小部件配置Activity。此Activity将由App Widget host自动启动,并允许用户在创建时配置App Widget的可用设置,例如App Widget颜色,大小,更新周期或其他功能设置。

配置Activity应该在Android manifest文件中声明为普通Activity。但是,App Widget host将使用ACTION_APPWIDGET_CONFIGURE操作启动它,因此Activity需要接受此Intent。例如:

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

同样,必须在AppWidgetProviderInfo XML文件中使用android:configure属性声明该Activity(参考上面的添加AppWidgetProviderInfo元数据),比如:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    ... >
</appwidget-provider>

请注意,Activity是使用完整的命名空间(fully-qualified namespace)声明的,因为它将从包范围外引用。

这就是开始使用配置Activity所需的全部内容,现在您只需要实现该Activity,在实现Activity时,需要记住两件重要的事情:

  • App Widget host调用配置Activity,配置Activity应始终返回结果。结果应该包括由启动Activity时Intent传递的App Widget ID(在Intent extras中保存为EXTRA_APPWIDGET_ID)。
  • 创建App Widget时不会调用onUpdate()方法(系统在启动配置Activity时不会发送ACTION_APPWIDGET_UPDATE广播)。首次创建App Widget时,配置Activity负责从AppWidgetManager请求更新。但是,将为后续更新调用onUpdate()——它仅在第一次被跳过。

有关如何从配置返回结果并更新App Widget的示例,请参阅以下部分中的代码片段。

在配置(configuration)Activity中更新App小部件

当App Widget使用配置Activity时,Activity负责在配置完成时更新App Widget。您可以通过直接从AppWidgetManager请求更新来完成此操作。

以下是正确更新App Widget并关闭配置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);
}

2:执行App Widget配置。 3:配置完成后,通过调用getInstance(Context)获取AppWidgetManager的实例:

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4:通过调用updateAppWidget(int,RemoteViews)使用RemoteViews更新App小部件布局:

RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

5:最后,创建返回的Intent,将其设置为Activity的返回结果,并finish Activity

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

提示:首次打开配置Activity时,将Activity结果与EXTRA_APPWIDGET_ID一起设置为RESULT_CANCELED,如上面的步骤5所示。这样,如果用户在到达结束之前退出Activity,则会通知App Widget host 配置已取消,并且不会添加App Widget。

查看ExampleAppWidgetConfigure.java示例Demo

设置预览图像

Android 3.0引入了previewImage字段,该字段指定应用程序小部件的外观预览。此预览将通过窗口小部件选择器显示给用户。如果未提供此字段,则应用小部件的图标将用于预览。

这是您在XML中指定此设置的方式:

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

为了帮助您为应用小部件创建预览图像(在previewImage字段中指定),Android模拟器包含一个名为“小部件预览(Widget Preview)”的应用程序。要创建预览图像,请启动此应用程序,为您的应用程序选择应用程序窗口小部件,并将其设置为您希望显示预览图像的方式,然后保存并将其放在应用程序的drawable 资源中。

将App小部件和集合一起使用

Android 3.0引入了带有集合的app小部件。这些类型的App Widgets使用RemoteViewsService显示来自如content provider这类远程数据支持的集合。 RemoteViewsService提供的数据使用以下视图类型之一显示在app小部件中,我们将其称为“集合视图”:

  • ListView

    一个视图,显示垂直滚动列表中的项目。有关示例,请参阅Gmail应用小部件。

  • GridView

    在二维滚动网格中显示项目的视图。有关示例,请参阅“书签”应用小部件。

  • StackView

    堆叠的卡片视图(类似于rolodex),用户可以分别向上/向下轻弹前卡以查看上一张/下一张卡片。示例包括YouTube和图书应用小部件。

  • AdapterViewFlipper

    支持适配器的简单ViewAnimator,可在两个或多个视图之间进行动画处理。一次只能展示一个子视图。

如上所述,这些集合视图显示由远程数据支持的集合。这意味着他们使用Adapter将用户界面绑定到他们的数据,Adapter将一组数据中的各个项绑定到单个View对象中。由于这些集合视图由Adapter支持,因此Android框架必须包含额外的体系结构以支持它们在app小部件中的使用。在app小部件的上下文中,Adapter被RemoteViewsFactory取代,它只是Adapter接口的一个简易包装器。当请求集合中的特定项时,RemoteViewsFactory将创建集合的项并将其作为RemoteViews对象返回。要在应用小部件中包含集合视图,您必须实现RemoteViewsService和RemoteViewsFactory。

RemoteViewsService是一种允许远程Adapter请求RemoteViews对象的服务。 RemoteViewsFactory是集合视图(如ListView,GridView等)与该视图的基础数据之间的Adapter的接口。以下是StackWidget示例项目中用于实现此服务和接口的代码的示例:

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 StackWidget sample.

}

示例应用程序

本节中的代码摘录摘自StackWidget sample

StackWidget

此示例包含10个视图的堆栈,显示从“0!”到“9!”,示例应用小部件具有以下主要行为:

  • 用户可以丢弃(fling)app小部件中垂直投影的顶视图以显示下一个或上一个视图。这是StackView内置的一个行为。
  • 在没有任何用户交互的情况下,app小部件会自动按顺序排列其视图,如幻灯片放映。这是由于在res / xml / stackwidgetinfo.xml文件中设置了android:autoAdvanceViewId =“@ id / stack_view”。此设置适用于视图ID,在本例中是堆栈视图的视图ID。
  • 如果用户触摸顶视图,则app小部件显示Toast消息“Touched view n”,其中n是触摸视图的索引(位置)。有关如何实现此操作的更多讨论,请参阅给各个项添加行为

实现app集合小部件

要实现app集合小部件,请遵循用于实现任何app小部件的相同基本步骤。以下部分描述了使用实现app集合小部件所需执行的其他步骤。

App集合小部件的Manifest文件

除了在manifest中声明应用程序窗口小部件中列出的要求之外,为了使app集合小部件可以绑定到RemoteViewsService,您必须使用权限BIND_REMOTEVIEWS在清单文件中声明该服务(RemoteViewsService)。这可以防止其他应用程序自由访问应用程序窗口小部件的数据。例如,在创建使用RemoteViewsService填充集合视图的App Widget时,清manifest条目可能如下所示:

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

MyWidgetService是一个RemoteViewService的子类。

App集合小部件的布局(layout)

应用程序窗口小部件布局XML文件的主要要求是它包含一个集合视图:ListView,GridView,StackView或者AdapterViewFlipper,这里是StackWidget 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 views)必须是集合视图( collection view)的兄弟,其中空视图表示空状态。

除了整个应用程序窗口小部件的布局文件之外,还必须创建另一个布局文件,该文件定义集合中每个项目的布局(例如,书籍集合中每本书的布局)。 StackWidget sample中只有一个布局文件widget_item.xml,因为所有项目都使用相同的布局。

App集合小部件的AppWidgetProvider类

与常规应用程序窗口小部件一样,AppWidgetProvider子类中的大部分代码通常都在onUpdate()中。在使用App集合小部件时,onUpdate()实现的主要区别在于您必须调用setRemoteAdapter(),这告诉集合视图获取其数据的位置。然后,RemoteViewsService可以返回RemoteViewsFactory的实现,并且窗口小部件可以提供适当的数据。调用此方法时,必须传递指向RemoteViewsService实现的intent和指定要更新的app小部件的app widget ID。

例如,以下是StackWidget示例如何实现onUpdate()回调方法,以将RemoteViewsService设置为应用程序窗口小部件集合的远程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子类提供用于填充远程集合视图的RemoteViewsFactory。

具体来说,您需要完成以下步骤:

  • 子类RemoteViewsService。 RemoteViewsService是远程适配器可以通过其请求RemoteView的服务。
  • 在RemoteViewsService子类中,包含一个实现RemoteViewsFactory接口的类。 RemoteViewsFactory是远程集合视图(如ListView,GridView等)与该视图的基础数据之间的Adapter的接口。您的实现负责为数据集中的每个项目创建RemoteViews对象。此接口是Adapter周围的简易包装器。

注意:您不能依赖Service的单个实例或其包含的任何数据来做持久化。因此,您不应在RemoteViewsService中存储任何数据(除非它是静态的)。如果您希望应用程序窗口小部件的数据保持不变,最好的方法是使用生命周期超出进程生命周期的ContentProvider。

RemoteViewsService实现的主要内容是RemoteViewsFactory,如下所述。

RemoteViewsFactory接口

实现RemoteViewsFactory接口的自定义类为应用程序窗口小部件提供其集合中项目的数据。为此,它将您的应用小部件item的XML布局文件与数据源相结合,这个数据源可以是从数据库到简单数组的任何数据。在StackWidget示例中,数据源是WidgetItems的数组。 RemoteViewsFactory用作将数据粘贴到远程集合视图的Adapter。

您需要为RemoteViewsFactory子类实现的两个最重要的方法是onCreate()和getViewAt()。

第一次创建工厂(factory)时,系统会调用onCreate()。您可以在此处为数据源设置任何连接和/或游标。例如,StackWidget示例使用onCreate()来初始化WidgetItem对象的数组。当您的应用程序窗口小部件处于生效状态时,系统将使用其在数组中的索引位置访问这些对象,并显示它们包含的文本。

以下是StackWidget示例的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对象。以下是StackWidget示例的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;
}

给单个项添加行为

以上部分介绍如何将数据绑定到App集合小部件。但是,如果要将动态行为添加到集合视图中的各个项目,该怎么办?

使用AppWidgetProvider类中所述,通常使用setOnClickPendingIntent()来设置对象的单击行为,例如使按钮启动Activity。但是,集合项中的单个子视图不允许使用此方法(为了澄清(clarify),您可以使用setOnClickPendingIntent()在启动应用程序的Gmail应用程序窗口小部件中设置全局按钮监听,但不能在单个列表项上设置)。要将单击行为添加到集合中的各个项目,请使用setOnClickFillInIntent(),这需要为您的集合视图设置待定的Intent模板(intent template),然后通过RemoteViewsFactory为集合中的每个项目设置填充意图(fill-in intent )。

本节使用StackWidget示例来描述如何向单个项添加行为。在StackWidget示例中,如果用户触摸顶视图,则app小部件显示Toast消息“Touched view n”,其中n是触摸视图的索引(位置)。这是它的工作原理:

  • StackWidgetProvider(AppWidgetProvider子类)创建一个挂起(pending )的intent,它具有一个名为TOAST_ACTION的自定义操作。
  • 当用户触摸View时,Intent被触发并广播TOAST_ACTION。
  • StackWidgetProvider的onReceive()方法拦截此广播,app小部件显示触摸视图的Toast消息。集合项的数据由RemoteViewsFactory通过RemoteViewsService提供。

注意:StackWidget示例使用广播,但通常app小部件会在类似这样的场景中启动Activity。

设置待定的意图模板(intent template)

StackWidgetProvider(AppWidgetProvider子类)设置挂起的Intent,集合中的单个项无法设置自己的待处理Intent,集合会作为一个整体设置一个待定的意图模板,并且各个项目设置一个填充Intent,以逐项创建唯一的行为。

该类还接收用户触摸视图时发送的广播。它在onReceive()方法中处理此事件。如果intent的操作是TOAST_ACTION,则app小部件会显示当前视图的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";

    ...

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

设置填充Intent

您的RemoteViewsFactory必须为集合中的每个项目设置填充Intent,这使得可以区分给定项目的单独点击动作,然后将填充意图与PendingIntent模板组合,以确定单击该项时将执行的最终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;
        }
    ...
    }

保持集合数据的有效性

下图说明了在发生更新时使用App集合小部件中发生的流程。它显示了应用程序窗口小部件代码如何与RemoteViewsFactory交互,以及如何触发更新:

appwidget_collections

使用集合的app小部件的一个功能是为用户提供最新内容的能力。例如,考虑Android 3.0 Gmail应用小部件,该小部件为用户提供其收件箱的快照。为了实现这一点,您需要能够触发RemoteViewsFactory和集合视图来获取和显示新数据。您可以使用AppWidgetManager调用notifyAppWidgetViewDataChanged()来实现此目的。此调用导致对RemoteViewsFactory的onDataSetChanged()方法的回调,这使您有机会获取任何新数据。请注意,您可以在onDataSetChanged()回调中同步执行处理密集型操作。您可以保证在从RemoteViewsFactory获取元数据或视图数据之前完成此调用。此外,您可以在getViewAt()方法中执行处理密集型操作。如果此调用需要很长时间,则加载视图(由RemoteViewsFactory的getLoadingView()方法指定)将显示在集合视图的相应位置,直到它返回为止。