Android小部件-AppWidget

App Widgets是一些较小的应用程序窗口,它们能够被嵌入到其他的应用程序中(如主屏窗口),并且能够接受周期性的更新。这些小窗口可以作为用户界面中的一个可视部件,而且这些可视部件也可以带有自己的App Widget提供器。能够持有其他App Widgets的组件被叫做App Widget的持有者。以下截图是Music App Widget。


本文介绍如何发布一个使用App Widget提供器的App Widget。对于创建自己的AppWidgetHost来持有AppWidgets,请看AppWidget Host。

Widget设计

对于如何设计应用可视部件,请阅读Widget设计指南。

基础

以下是创建一个App Widget所要了解的基本内容:

  • AppWidgetProviderInfo对象

    这个对象用于描述App Widget的元数据,如布局、更新周期以及AppWidgetProvider类等。它应该在XML文件中定义。

  • 实现AppWidgetProder类

    这个类定义了一些App Widget所带有的基本的、基于广播事件的编程接口方法,通过这个类,当App Widget被更新、启用、禁用和删除时,你会收到广播事件。

  • 窗口布局

    在XML中给App Widget定影初始化布局。

另外,你还可以实现一个用于App Widget配置的Activity。这是一个可选的Activity,它会在用户添加该App Widget时启动,并允许用户在创建是编辑App Widget的设置。

以下介绍如何创建App Widget。

在清单中声明一个AppWidget

首先,在你的应用程序的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_UPDATE类型的广播。这是需要你明确声明的唯一的广播。必要时,AppWidgetManager会自动的把所有其他的App Widget广播发送给AppWidgetProvider。

<meta-data>元素指定了AppWidgetProviderInfo对象所要求的资源,以及以下必要的属性:

  • android:name-指定元数据名称。使用android.appwidget.provider作为AppWidgetProviderInof的描述符来标识数据。

  • android:resource---指定AppWidgetProviderInfo资源的位置。

添加AppWidgetProviderInfo元数据

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>元素属性的概要介绍

  • minWidth和minHeight属性值指定了AppWidget默认所要占据的空间大小。位于主屏窗口中的App Widget的尺寸是基于主屏中一个网格所定义的高度和宽度。如果App Widget的最小宽度或高度值跟网格的尺寸不匹配,那么App Widget的尺寸会四舍五入到最接近的单元格尺寸。有关App Widget尺寸的更多信息,请看AppWidget 设计指南。

    注意:如果要让你的App Widget适应更多的设备,那么它的最小尺寸就不应该大于4x4单元格。

  • minResizeWidth和minResizeHeight属性指定了AppWidget的绝对最小尺寸。这两个值应该在不能确定App Widget尺寸的情况下使用,或者不使用。这个两个属性会允许用户重新调整Widget的尺寸,它们的值可以比minWidth和minHeight属性定义的默认的Widget尺寸值小。它们在Android3.1中被引入。

    关于调整App Widget尺寸的更多信息,请看App Widget设计指南。

  • updatePeriodMillis属性定义了AppWidget框架的更新频率,App Widget通过调用onUpdate()回调方法来请求来自AppWidgetProvider的更新。实际的更新并不保证准确的按照这个值所定义的时间周期发生,并且我们建议尽可能减少更新频率---1小时最好不要超过一次,以便节省电池电量。你也可以通过配置允许用户来调整更新频率。有些人可能想要每隔15分钟看一次股票报价,也可能一天只看四次。

    注意:如果设备在休眠时发生了更新,那么为了执行更新,设备将会被唤醒。如果每小时的更新不多于1次,那么就不会对电池的寿命造成显著的影响。但是,如果你要频繁的并且(或者)在设备休眠时不需要更新,那么你可以使用闹钟来代替,它不会唤醒设备。使用AlarmManager对象,给闹钟设置一个你的AppWidgetProvider能够接收的Intent对象。设置的闹钟类型既可以是ELAPSED_REALTIME也可以是RTC,它们只会在设备清醒的时候发送闹钟。然后把updatePeriodMillis属性设置为0.

  • initialLayout属性指定了定义App Widget布局的布局资源。

  • configure属性定义了用户添加App Widget时要启动的Activity,以便用于设置AppWidget属性。这是可选的。

  • previewImage属性指定了配置以后的App Widget的外观的预览图片,这样用户在选择该App Widget时就可以先看到外观。如果没有设置这个属性,那么用户就只会看到应用程序的启动图标。这个属性对应了AndroidManifest.xml文件中<receiver>元素中的android:previewImage属性。更多的使用previewImage讨论,请看设置预览图片。这个属性在Android3.0中被引入。

  • autoAdvanceViewId属性指定了AppWidget的子View的ID,它应该是有Widget的持有者自动生成的,它在Android3.0中被引入。

  • resizeMode属性指定调整Widget尺寸的规则。使用这个属性使得主屏Widget 可以调整尺寸。用户按住一个Widget就会显示一个调整尺寸的手柄,然后水平或垂直拖动手柄来改变Widget的尺寸。这个属性值包括:horizontal、vertical、none。要同时调整水平和垂直的尺寸,可以把属性值设置为“horizontal|vertical”。这个属性在Android3.1中被引入。

  • minResizeHeight属性指定了Widget可以调整的最小的高度(单位:dps)。如果它的值比minHeight属性值大,或者垂直尺寸不可调整,那么这个属性无效。该属性在Android4.0中被引入。

  • minResizeWidth属性指定了Widget可以调整的最小的宽度(单位:dps)。如果它的值比minWidth属性值大,或者水平尺寸不可调整,那么这个属性无效。该属性在Android4.0中被引入。

  • widgetCategory属性声明了你的App Widget是否能够显示在主屏或锁定屏幕上,或者是在这两个屏幕上都显示。它属性值包括:home_screen、keyguard。要让Widget同时显示在这种屏幕上,就要确保Widget类遵守它们的设计指南。更多的信息,请看“让App Widget能够显示在锁屏上”。默认值是“home_screen”。该属性在Android4.2中被引入。

有关<appwidget-provider>元素所能接收的更多属性信息,请看AppWidgetProviderInfo类。

创建App Widget布局

你必须在XML文件中给你的App Widget定义一个初始布局,并把它保存在工程的res/layout目录中。你可以使用以下列出的View对象来设计你的App Widget,但是在开始设计之前,请阅读和理解AppWidget设计指南。

如果你熟悉布局,那么创建App Widget布局就很简单了。但是必须要注意的是:App Widget布局是基于RemoteViews,它不支持所有类型的布局和View。

RemoteView对象能够支持以下布局类:

  • FrameLayout

  • LinearLayout

  • RelativeLayout

  • GridLayout

以及以下可视组件类:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextView

  • ViewFlipper

  • ListView

  • GridView

  • StackView

  • AdapterViewFlipper

这些类的派生类是不被支持的。

RemoteView类也提供了ViewStub,它是一个不可见的、尺寸为0的View,你可以使用它在运行时来慢慢的填充布局。

给App Widget添加边距

通常Widget不应该超出屏幕的边缘,而且在视觉上不应该与其他的Widget平齐,因此你应该在你的Widget所有边框周围添加边距。

从Android4.0开始,App Widget会自动的在Widget的边框和边界之间添加边距,以便更好的与其他的Widget和主屏上的图标对齐。要获取这种好处,强烈推荐你的应用程序所使用的SDK版本在14以上。

写一个能够适应早期版的有定制边距的单一布局,并且让它在Android4.0以后的版本中不会有额外的边距是很容的:

  • 把应用程序的targetSdkVersion属性设置为14或更高的版本;

  • 创建如下的布局,让它引用一个带有边距的尺寸资源:

<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>
  • 创建两个尺寸资源,一个在res/values/目录中,它给Android4.0之前的版本定制边距,另一个在res/values-v14/目录中,它给不给Android4.0以后的Widget提供额外的边距。

    res/values/dimens.xml
<dimen name="widget_margin">8dp</dimen>

res/values-v14/dimens.xml:

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

另一种选择是简单的创建一个带有边距的默认的九宫格背景图,并且再给APILevel14以后的版本提供一个没有边距的九宫格背景图。

使用AppWidgetProvider类

AppWidgetProvider类继承BroadcastReceiver类,它能够方便的处理AppWidget广播。AppWidgetProvider只接收与App Widget相关的的事件广播,如App Widget被更新、被删除、被启用、以及被禁用。当这些广播事件发生时,AppWidgetProvider会接收以下方法的调用:

  • onUpdate()

    在指定的时间间隔内调用这个方法来更新App Widget,这个时间间隔通过AppWidgetProviderInfo中的updatePeriodMillis属性来定义。当用户添加App Widget时这个方法也会被调用,因此它应该执行基本的安装,如给View定义事件处理器,以及如果需要,启动临时的Service。但是如果你声明了用于配置的Activity,那么当用户添加App Widget时,这个方法就不会被调用,但随后的更新会被调用。当配置完成后,配置Activity会负责执行第一次更新。

  • onAppWidgetOptionsChanged()

    在第一次放置Widget时,以及在调整Widget尺寸的时候,这个方法就会被调用。你可以使用这个回调方法,基于Widget的尺寸来显示和隐藏内容。通过调用getAppWidgetOptionss()方法来获取Widget的尺寸范围,它会返回一个包含以下信息的Bundle对象:

    • OPTION_APPWIDGET_MIN_WIDTH---以dp为Widget距离单位,控制当前宽度的下限。

    • OPTION_APPWIDGET_MIN_HEIGHT---以dp为Widget距离单位,控制当前高度的下限。

    • OPTION_APPWIDGET_MAX_WIDTH---以dp为Widget距离单位,控制当前宽度的上限。

    • OPTION_APPWIDGET_MAX_HEIGHT---以dp为Widget距离单位,控制当前高度的上限。

这个回调方法在API Level 16(Android4.1)中被引入。如果你实现了这个回调方法,就要确保你的app不会对它形成依赖,因为在较早版本上这个方法不会被调用。

  • onDeleted(Context,int[])

    每次从App Widget的持有者中删除该App Widget时,都会调用这个方法。

  • onEnabled(Context)

    当第一次创建App Widget实例时,这个方法会被调用。例如,如果用户添加了两个相同的App Widget实例,那么这个方法只会在第一次创建App Widget实例时才会被调用。如果你需要打开一个新的数据库,或者那些只要执行一次的操作,那么使用这个回调方法时一个比较好的地方。

  • onDisabled(Context)

    当从App Widget持有者中删除最后一个你的App Widget时,这个方法会被调用,在这个方法中,应该执行一些清理工作,如删除临时的数据库等。

  • onReceive(Context,Intent)

    每个广播在上述的回调方法之前会调用这个方法,通常你不需要实现这个方法,因为默认的AppWidgetProvider实现了对所有的App Widget广播的过滤,并且会在适当的时候调用上述的回调方法。

最重要的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);
        }
    }
}

这个AppWidget只定义了onUpdate()方法,启动定义了一个用于启动Activity的PendingIntent对象,并且用setOnClickPendingIntent(int, PendingIntent)方法把它绑定到App Widget的按钮上。注意,onUpdate()方法中使用了一个循环来遍历appWidgetIds中的每个实体,appWidgetIds中包含了由提供器创建的每个App Widget实例的标识ID。用这种方法,如果用户创建了多个App Widget实例,那么所有实例的更新都是同时执行的。但是,只有一个updatePeriodMillis计划表来管理所有的App Widget实例。例如,如果更新计划被定义成每两小时一次,并且第二App Widget实例是在第一个之后一小时添加的,那么这两个实例都会使用第一个实例所定义的更新周期,而第二实例所定义的更新周期将会被忽略。

注意:因为AppWidgetProvider类继承BroadcastReceiver类,所以在回调方法返回之后,你的处理并不保证会保持运行的状态(有关广播生命周期的信息请看BroadcastReceiver类)。如果安装你的App Widget进程需要花费几秒钟的时间(可能是因为要执行一个Web请求),并且你要求你的处理要保持连续性,那么就要考虑在onUpdate()方法中启动一个Service。在这个Service中,你可以执行对App Widget的更新,而不用担心因应用无响应(ANR)的错误而关闭AppWidgetProvider。App Widget运行Service例子请看AppWidgetProvider示例。

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

创建AppWidget的配置Activity

如果在用户添加一个新的App Widget时,你想要用户完成一些设置,那么你可以创建一个用于配置的Activity。这个Activity会由App Widget的持有者自动的启动,并允许用户在创建时给App Widget做一些有效的设置,如App Widget的颜色、大小、更新周期或其他的功能性设置。

这个配置Activity会作为一个普通的Activity在Android清单文件中声明。但是,它会由App Widget持有使用用ACTION_APPWIDGET_CONFIGURE操作来启动,因此这个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。但是,在实现你的Activity时要记住两件重要的事情:

  • App Widget持有者会调用这个配置Activity,并且这个配置Activity要始终返回结果。这个结果应该包括App Widget的ID(它保存在Intent对象的附加信息中,键名:EXTRA_APPWIDGET_ID)。

  • 在AppWidget被创建时,onUpdate()方法不会被调用(因为在配置Activity被启动时,系统不会发送ACTION_APPWIDGET_UPDATE广播)。当App Widget被首次创建时,这个配置Activity会请求来自AppWidgetManager的更新。但是,onUpdate()方法会被后续的更新调用---它只是在第一次被跳过。

以下章节会看到从配置Activity返回结果和更新App Widget的代码片段。

从配置Activity中更新App Widget

当App Widget使用一个配置Activity时,在配置完成后,它会负责更行App Widget。直接向AppWidgetManager发送请求来完成更新。

以下是一个简要的更新App Widget并关闭配置Activity的过程:

  • 从启动Activity的Intent中获取App WidgetID:
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布局的App Widget:
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);
  • 最后创建一个要返回的Intent对象,设置返回结果,并销毁当前Activity:
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

提示:在你的配置Activity被首次打开时,把Activity的设置结果设置给RESULT_CANCELED。如果用户在设置结束之前退出了该Activity,那么用这种方法就会通知App Widget的持有者,配置被取消了,并且App Widget将不会被添加。

设置预览图片

Android3.0以后引入了previewImage字段,它用于指定AppWidget外观的预览图片,它会在Widget选择器中显示。如果不支持这个字段,那么App Widget的图片会用于预览。

以下是在XML中指定这个设置的方法:

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

为了帮助你给App Widget创建预览图片,Android模拟器包含了一个叫做“Widget Preview”的应用程序。启动这个应用程序,给你的应用程序选择App Widget,并且给它创建要显示的预览图片,就可以创建一个App Widget的预览图片,然后保存它,并把它放到你的应用程序的可绘制资源中。

使用带有集合的App Widget

Android3.0以后引入了带有集合的App Widget。这些类型的App Widget使用RemoteViewService来显示由远程数据所返回的数据集合,如来自contentprovider的数据。由RemoteViewsService提供的数据会被显示在下列类型之一的View中,我们把这些View叫做“集合View”:

  • ListView

    在一个垂直滚动的列表中显示数据的View。例如,Gmail的App Widget

  • GridView

    在一个二维的可滚动的网格中显示数据的View。例如,Bookmarks的App Widget

  • StackView

    一个堆放卡片的View(有点象关系网),用户可向下或向上来抽取卡片,以便分别的的看前一张或下一张卡片。例如,YouTube和Books中包含的App Widget。

  • AdapterViewFilpper

    一个支持简单ViewAnimator的适配器View,它可以在两个或更多的View之间产生动画。每次只显示一个子View。

在上述的View中,会显示由远程数据所返回的数据集。这就意味着它们要使用一个Adapter把数据跟用户界面绑定。Adapter会把数据集中的每个数据项绑定到每个View对象上。因为这些集合View是由Adapter所支持的,所以Android框架必须包含额外的架构来支持在App Widget中使用它们。在App Widget的内容中,Adapter会被RemoteViewsFactory替代,它只是简单的封装了Adapter的接口。当请求集合中的一个特殊项目时,RemoteViewsFactory会给集合创建并返回一个RemoteViews对象。为了在你的App Widget中包含一个集合View,你必须要实现RemoteViewsService和RemoteViewsFactory接口。

RemoteViewsService是一个服务,它允许远程的适配器来请求RemoteViews对象。

RemoteViewsFactory是一个在集合View和View相关的数据之间的适配器接口,以下代码代码来自StackView Widget示例,它是你用于实现服务和接口的样板代码:

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.

}

示例

下面部分的代码来源于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。

用集合实现应用程序小部件

使用容器实现app widget,必须依照下述步骤。

  • 用集合实现小部件之清单文件

    除了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的相关子类。

  • 用集合实现小部件之布局

    此类app widget layout XML文件的唯一要求是包含下述容器视图之一:ListView, GridView,StackView或AdapterViewFlipper。如下是StackView Widget sample的widget_layout.xml:


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

    通常编写一个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) {
    // 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类

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

    • RemoteViewsService子类是远程adapter要求RemoteViews的地方。

    • 在RemoteViewsService子类中,包含一个实现了RemoteViewsFactory接口的类。RemoteViewsFactory是为了适配远程容器视图(如ListView, GridView)和提供给该视图的数据所设计的接口。那么这种实现就必须为data set中的每项提供RemoteViews对象。这个接口是Adapter的简单封装。

  • RemoteViewsFactory接口

    实现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() {
        // 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 + "!"));
        }
        ...
    }
...
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;

为单独项目添加行为

上面部分展示了绑定数据到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提供。

StackView Widget样例使用了广播,通常一个典型的app widget会简单的启动一个activity。

设置挂起的Intent模板

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

}

设置替补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;
        }
    ...
    }

保持采集数据新鲜

下图描述了更新时使用容器的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()方法指定)将会在容器视图的响应位置展示知道返回。

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1