Android API指南之应用程序窗口小部件

本文与公众号三七文档库同步。

本文由三七原创翻译,转载前务必联系三七。

应用程序窗口小部件是微型应用程序视图,可以嵌入到其他应用程序(如主屏幕)中,并定期接收更新。这些视图在用户界面中称为小部件,您可以使用应用程序窗口小部件提供程序发布视图。能够容纳其他应用程序窗口小部件的应用程序组件被称为应用程序窗口小部件主机。以下屏幕截图显示了音乐应用程序窗口小部件。

本文档介绍如何使用应用程序窗口小部件提供程序发布应用程序窗口小部件。有关创建自己个人托管应用小程序小部件主机AppWidgetHost 的讨论,请参见应用程序窗口小部件 Host

小部件设计

有关如何设计应用程序窗口小部件的信息,请阅读小部件设计指南

基础知识

要创建一个应用程序窗口小部件,您需要以下内容:

AppWidgetProviderInfo 对象

描述应用程序窗口小部件的元数据,例如应用程序窗口小部件的布局,更新频率和AppWidgetProvider类。这应该在XML中定义。

AppWidgetProvider 类的实现

定义了基本方法,允许您基于广播事件与应用程序窗口小部件进行交互。通过它,你将在应用程序窗口小部件更新、启用、禁用和删除时收到广播。

视图布局

在XML文件中定义应用程序窗口小部件的初始布局。

另外,您可以实现一个应用程序窗口小部件配置Activity。这是一个可选的Activity,当用户添加您的应用程序窗口小部件时允许他/她在创建时修改小部件设置。

以下部分介绍如何设置每个组件。

在清单中声明应用程序窗口小部件

首先,在您应用的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属性,这个属性指定了应用程序窗口小部件使用的AppWidgetProvider

<intent-filter>元素必须包含一个拥有android:name属性的<action>元素。这个属性指定了用来接收ACTION_APPWIDGET_UPDATE广播的AppWidgetProvider。这是唯一一个你必须明确声明的广播。AppWidgetManager根据需要自动将所有其他应用程序窗口小部件的广播发送到AppWidgetProvider。

<meta-data>元素指定的AppWidgetProviderInfo资源,需要以下属性:

添加AppWidgetProviderInfo元数据

AppWidgetProviderInfo定义了应用程序窗口小部件的重要特征,比如它的最小布局尺寸,初始布局资源,小部件更新频率和创建时打开的(可选的)配置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>属性的摘要:

  • minWidthminHeight属性的值指定了应用程序窗口小部件默认使用的最小空间量。默认的主屏幕基于定好了高度和宽度的单元网格来在其窗口中放置应用程序窗口小部件。如果应用程序窗口小部件的最小宽度或高度值与单元格的尺寸不匹配,则应用程序的尺寸向上舍入到最接近的单元格尺寸。

    有关调整App Widgets的更多信息,请参阅应用程序窗口小部件设计指南

    注意: 要使您的应用程序窗口小部件可跨设备使用,应用程序窗口小部件的最小大小决不能大于4 x 4个单元格。

  • minResizeWidthminResizeHeight属性指定应用程序窗口小部件的最小绝对尺寸。这些定义尺寸的值应该低于让应用程序窗口小部件难以辨认或不可用的值。使用这些属性允许用户将小部件的大小调整为可能小于minWidth和由minHeight属性定义的默认窗口大小的大小 。在Android 3.1中引入。

    有关调整App Widgets的更多信息,请参阅应用程序窗口小部件设计指南

  • updatePeriodMillis属性定义了应用程序窗口小部件框架通过调用 onUpdate()回调方法来从AppWidgetProvider请求更新的频率。实际更新不能保证按照这个值准确发生,我们建议尽可能不要频繁更新 - 也许每小时不要超过一次,以节约电量。您也可以允许用户调整配置中的频率 - 有些人可能希望每15分钟更新股票代码,或者每天只更新四次。

    注意:如果设备在进行更新(按照updatePeriodMillis定义的)时处于睡眠状态,则设备将被唤醒以执行更新。如果不是每小时更新一次以上,这可能不会对电池寿命造成严重的问题。但是,如果需要更频繁地更新和/或在设备处于睡眠状态时不需要更新,则可以改为基于不会唤醒设备的闹钟进行更新。为此,使用AlarmManager来设置一个闹钟,这个闹钟需要带有一个你的AppWidgetProvider可以接收的Intent。设置闹钟类型为ELAPSED_REALTIMERTC,只有在设备唤醒时才会发出闹钟。然后设置 updatePeriodMillis为零("0")。

  • initialLayout属性指向定义应用程序窗口小部件布局的布局资源。

  • configure属性定义用户在添加应用程序窗口小部件时要启动的Activity,以便他/她配置应用程序窗口小部件属性。这是可选的(请阅读下面的创建应用程序窗口小部件配置Activity
  • previewImage属性指定应用程序窗口小部件的配置样式预览,这是用户在选择应用程序窗口小部件时看到的内容。如果没有提供,用户会看到您的应用程序的启动器图标。该字段对应于AndroidManifest.xml文件中<receiver>元素的android:previewImage属性。请参阅设置预览图像。在Android3.0中引入。
  • autoAdvanceViewId属性指定应由小部件主机自动升级的应用程序窗口小部件子视图的视图ID。在Android3.0中引入。
  • resizeMode属性指定了小部件可以调整大小的规则。您可以使用这个属性来设置主屏幕小部件水平,垂直或两个轴的大小可调整性。用户按住一个小部件以显示其大小调整手柄,然后拖动水平/垂直手柄以更改其在布局网格上的大小。resizeMode 属性的值包括horizontalverticalnone。为了声明一个小部件可以调整水平和垂直方向的大小,请提供值horizontal|vertical。在Android3.1中引入。
  • minResizeHeight属性指定小部件可以调整的最小高度(以dps为单位)。如果此字段大于minHeight或未启用垂直大小调整,则此字段无效(请参阅resizeMode)。在Android4.0中引入。
  • minResizeWidth属性指定小部件可以调整的最小宽度(以dps为单位)。如果此字段大于minWidth或未启用水平大小调整,则此字段无效(请参阅resizeMode)。在Android4.0中引入。
  • widgetCategory属性声明您的应用程序窗口小部件s会否可以显示在主屏幕(home_screen),锁屏(keyguard)或两者上。只有低于5.0的Android版本才支持锁屏小部件。对于Android5.0及更高版本,只有home_screen有效。

有关<appwidget-provider>元素能接受的属性的更多信息,请参阅AppWidgetProviderInfo类。

创建应用程序窗口小部件布局

您必须使用XML定义一个你的应用程序窗口小部件的初始布局并将其保存在res/layout/文件夹中。您可以使用下面列出的View对象来设计您的应用程序窗口小部件,但在开始设计之前,请阅读并理解应用程序窗口小部件设计指南

如果您熟悉布局,创建应用程序窗口小部件会很简单。但是,您必须知道,应用程序窗口小部件布局是基于不支持任何类型的布局或窗口小部件的RemoteViews类。

RemoteViews对象(以及基于此的应用程序窗口小部件)可以支持以下布局类:

以下小部件类:

不支持这些类的子类。

RemoteViews还支持ViewStub,这是一个不可见的,大小为零的视图,您可以使用它在运行时延迟布局资源。

为应用程序窗口小部件添加边距

小部件通常不应该扩展到屏幕边缘,并且不应该在视觉上与其他小部件齐平,所以您应该在小部件框架的周围添加边距。

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

编写一个适用于早期版本的平台的自定义边距的单一布局很容易,而Android 4.0和更高版本没有额外的边距:

  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/中用于提供Android 4.0之前的自定义边距,另一个放在res/values-v14/中用于Android 4.0的窗口小部件且不提供额外的边距。

    res/values/dimens.xml:

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

    res/values-v14/dimens.xml:

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

另一个选择是在默认情况下简单地把额外的边距放到.9图中,并为API级别14或更高版本提供不同的.9图,而没有空白。

使用AppWidgetProvider类

您必须使用<receiver>元素在AndroidManifest(请参阅上面的在清单文件中定义应用程序窗口小部件)中将您的AppWidgetProvider类的实现声明为一个广播接收器。

AppWidgetProvider类继承自BroadcastReceiver,作为一个帮助类来处理应用程序窗口小部件的广播。这个AppWidgetProvider类只接收应用程序窗口小部件相关的广播事件,比如当应用程序窗口小部件被更新、删除、启用、禁用时。发生这些广播事件时,AppWidgetProvider会收到以下方法调用:

onUpdate()

这个被调用来以updatePeriodMillis属性定义的间隔更新应用程序窗口小部件(请参阅上面的添加AppWidgetProviderInfo元数据)。这个方法也会在用户添加应用程序窗口小部件时调用,所以它应该执行必要的设置,比如为Views定义事件处理器,并在必要时启动一个临时的Service。但是,如果您声明了配置Activity,则在用户添加应用程序窗口小部件时不会调用此方法,但会为后续更新调用此方法。配置完成后,执行第一次更新是配置Activity的职责。(请参阅下面的创建一个应用程序窗口小部件配置Activity

onAppWidgetOptionsChanged()

这是在小部件第一次放置时以及小部件被调整大小时调用的。您可以使用此回调来显示或隐藏基于窗口小部件大小范围的内容。你可以通过调用getAppWidgetOptions()来获取大小范围,返回的Bundle包括以下内容:

此回调是在API级别16(Adnroid 4.1)中引入的。如果您实现此回调,请确保您的应用程序不依赖它,因为它不会在较旧的设备上调用。

onDeleted(Context, int[])

每当从应用程序窗口小部件主机删除一个小部件,就会调用它。

onEnabled(Context)

这是第一次创建应用程序窗口小部件的实例时调用的。例如,如果用户添加了两个您的应用程序窗口小部件的实例,这个方法只会在第一次抵用。如果您需要打开一个新的数据库或执行其他设置,对于所有的应用程序窗口小部件实例只需要执行一次的,那么这是一个很好的位置来做这些。

onDisabled(Context)

当您的应用程序窗口小部件的最后一个实例从应用程序窗口小部件主机中被删除时调用。您应该在这里清理所有在onEnabled(Context)`中完成了的工作,比如删除一个临时的数据库。

onReceive(Context, Intent)

每个广播都会调用并且在所有上述回调方法之前调用。你一般不需要实现这个方法,因为默认的AppWidgetProvider实现会过滤所有的窗口小部件广播并根据需要调用上述方法。

最重要的AppWidgetProvider回调方法是onUpdate(),因为它在每个应用程序窗口小部件添加到主机时都会调用(除非你使用了配置Activity)。如果您的应用程序窗口小部件需要接收任何用户交互事件,则需要在此回调中注册事件处理器。如果您的应用程序窗口小部件不需要创建临时的文件或数据库,或者执行其他需要清理的工作,则onUpdate() 可能是您需要定义的唯一一个回调方法。例如,如果您想要一个带一个按钮的应用程序窗口小部件,点击后可以启动一个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 应用程序窗口小部件 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 应用程序窗口小部件 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 应用程序窗口小部件
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

这个AppWidgetProvider只定义了一个onUpdate()方法来定义一个PendingIntent启动Activity并通过setOnClickPendingIntent(int, PendingIntent)把它绑定到应用程序窗口小部件的按钮上。请注意,它包含一个循环遍历每个appWidgetIds项目,这是一个标识每个由此提供者创建的应用程序窗口小部件的ID数组。这样,如果用户创建多个应用程序窗口小部件实例,那么他们是同时更新的。但是,对于所有的应用程序窗口小部件实例,只有一个updatePeriodMillis时间表将被管理。例如,如果更新时间表定义为每两个小时,并且在一个小时后第二个应用程序窗口小部件实例被添加进来了,那么它们将以第一个实例定义的周期更新,第二个实例定义的的更新周期将被忽略(它们将每两小时更新一次,而不是每一小时)。

注意: 因为AppWidgetProviderBroadcastReceiver的一个子类,您的进程不保证继续运行(请参阅BroadcastReceiver有关广播生命周期的信息)。如果您的应用程序窗口小部件安装过程可能需要几秒钟(可能正在执行网络请求),并且您需要继续执行,请考虑在onUpdate()方法中启动一个Service。从服务内部,您可以执行您自己的应用程序窗口小部件更新,而不用担心由于应用程序未响应(ANR)错误导致AppWidgetProvider关闭。请参阅Wiktionary示例的AppWidgetProvider,以获取在应用程序窗口小部件中运行服务的例子。

另请参阅ExampleAppWidgetProvider.java示例类。

接收应用程序窗口小部件的广播Intent

AppWidgetProvider只是一个辅助类。如果你想直接接收应用程序窗口小部件的广播,你可以实现你自己的BroadcastReceiver或重写onReceive(Context, Intent) 回调。你需要关心的Intent如下:

固定的应用程序窗口小部件

在运行Android 8.0(API级别26)及更高版本的设备上,启动器允许您创建固定快捷方式,也允许您将应用程序窗口小部件固定到启动器上。与固定的快捷方式类似,这些固定的小部件可让用户访问应用程序中的特定任务。

在您的应用程序中,您可以创建一个系统请求,通过完成以下一系列步骤将小部件固定到支持的启动器上:

  1. 在应用程序的清单文件中创建小部件,如下面的代码片段所示:
<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>
  1. 调用requestPinAddWidget()方法,如一下代码片段所示:
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作为第三个参数 传入requestPinAddWidget()

创建一个应用程序窗口小部件配置Activity

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

配置Activity应该在Android清单文件中声明为普通的Activity。但是,它将由应用程序窗口小部件主机通过ACTION_APPWIDGET_CONFIGURE动作来启动,所以Activity需要接受这个Intent。例如:

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

此外,必须在XML文件的AppWidgetProviderInfo元素中使用android:configure属性(请参阅上面的添加AppWidgetProviderInfo元数据)来声明Activity 。例如,配置Activity可以像这样声明:

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

请注意,该Activity是使用完全限定的命名空间声明的,因为它将从包的范围之外引用。

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

  • 应用程序窗口小部件主机调用配置Activity,配置Activity应始终返回结果。结果应该包括启动Activity的Intent所传递的应用程序窗口小部件ID(保存在Intent extras中 EXTRA_APPWIDGET_ID)。
  • 应用程序窗口小部件主机创建时不会调用onUpdate() 方法(当配置Activity启动时,系统不会发送ACTION_APPWIDGET_UPDATE广播)。当应用程序窗口小部件第一次被创建时,配置Activity负责从AppWidgetManager请求更新。但是,进行后续更新时onUpdate() 将被调用 - 它只是第一次被跳过。

有关如何从配置返回结果并更新应用程序窗口小部件的示例,请参阅以下部分的代码片段。

从配置Activity更新应用程序窗口小部件

当一个应用程序窗口小部件使用一个配置Activity时,Activity的职责是在配置完成后更新应用程序窗口小部件。您可以通过直接从AppWidgetManager请求更新来完成。

以下是正确更新应用程序窗口小部件并关闭配置Activity的过程摘要:

  1. 首先,从启动Activity的Intent中获取应用程序窗口小部件ID:

    Intent intent = getIntent();
    Bundle extras = intent.getExtras();
    if (extras != null) {
        mAppWidgetId = extras.getInt(
                AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }
    
  2. 执行您的应用程序窗口小部件的配置.

  3. 配置完成后,调用getInstance(Context)命令获取AppWidgetManager的一个实例 :

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    
  4. 通过调用updateAppWidget(int, RemoteViews)使用RemoteViews布局来更新应用程序窗口小部件:

    RemoteViews views = new RemoteViews(context.getPackageName(),
    R.layout.example_appwidget);
    appWidgetManager.updateAppWidget(mAppWidgetId, views);
    
  5. 最后,创建返回Intent,将其设置为Activity结果,并结束Activity。

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

提示:首次打开配置Activity时,将Activity结果设置为RESULT_CANCELED,同时使用EXTRA_APPWIDGET_ID,如上面的步骤5所示。这样,如果用户在到达结尾之前退出Activity,则会通知应用程序窗口小部件主机取消配置,并且不会添加应用程序窗口小部件。

以ApiDemos中的ExampleAppWidgetConfigure.java示例类为例。

设置预览图像

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

这是你如何在XML中指定这个设置:

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

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

使用带集合的应用程序窗口小部件

Android 3.0 引入了集合的应用程序窗口小部件。这些类型的应用程序窗口小部件使用RemoteViewsService显示由远程数据(例如来自内容提供者)支持的集合。由RemoteViewsService提供的数据使用以下类型之一的视图在应用程序窗口小部件中显示,我们将其称为“集合视图:”

ListView

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

GridView

显示二维滚动网格中的项目的视图。有关示例,请参阅Bookmarks应用程序窗口小部件。

StackView

一个堆叠的卡片视图(有点像名片盒),用户可以分别上下滑动前面的卡片来查看上一张/下一张卡片。示例包括 YouTube 和 Books 应用程序小部件。

AdapterViewFlipper

一个简单的支持适配器的ViewAnimator动画,它可以在两个或者更多视图之间执行动画。一次只显示一个子视图。

如上所述,这些集合视图显示由远程数据支持的集合。这意味着它们使用一个Adapter将用户界面绑定到它们的数据。一个 Adapter 绑定一组数据的各个数据项到单独的 View 对象上。由于适配器支持这些集合视图,所以Android框架必须包含额外的架构以支持其在应用程序窗口小部件中的使用。在应用程序窗口小部件的上下文中,Adapter被替换为一个RemoteViewsFactory,这仅仅是Adapter界面周围的一个简单包装。当在集合中请求特定项目时,RemoteViewsFactory创建并将集合项目作为RemoteViews对象返回。为了在您的应用程序窗口小部件中包含集合视图,您必须实现RemoteViewsServiceRemoteViewsFactory

RemoteViewsService是允许远程适配器请求RemoteViews对象的服务。RemoteViewsFactory是集合视图(例如ListViewGridView等)与该视图的基础数据之间的适配器的接口。下面是来自StackView窗口小部件示例的用于实现此服务和接口的样板代码示例:

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窗口小部件示例

该样品包含显示值为“0!”到“9!”的10个视图,示例应用程序窗口小部件有这些主要行为:

  • 用户可以垂直滑动应用程序窗口小部件中的顶部视图,以显示下一个或上一个视图。这是一个内置的StackView行为。
  • 在没有任何用户交互的情况下,应用程序窗口小部件会按顺序自动前进,就像幻灯片放映一样。这是这是由于android:autoAdvanceViewId="@id/stack_view"res/xml/stackwidgetinfo.xml文件中的设置。这个设置应用于视图ID,在这个例子中的视图ID是StackView的。
  • 如果用户触摸顶部视图,则应用程序窗口小部件显示Toast消息“触摸视图n”,其中n是所触摸的视图的索引(位置)。有关如何实现的更多讨论,请参阅将行为添加到单个项目

使用集合实现应用程序窗口小部件

要使用集合实现应用程序窗口小部件,您需要遵循与用于实现任何应用程序窗口小部件相同的基本步骤。以下部分描述了您需要执行的附加步骤来实现带集合的应用程序窗口小部件。

带集合的应用程序窗口小部件的清单文件

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

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

这行android:name="MyWidgetService指的是您的子类RemoteViewsService

带集合的应用程序窗口小部件的布局

对您的应用程序窗口小部件的XML布局文件的主要要求是它要包含一个集合视图:ListViewGridViewStackView,或AdapterViewFlipper。下面是StackView窗口小部件示例中的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>

请注意,空视图必须是表示空状态的空视图的集合视图的同胞。

除了整个应用程序窗口小部件的布局文件之外,还必须创建另一个布局文件来定义集合中每个项目的布局(例如,书籍集合中每个图书的布局)。例如,StackView窗口小部件示例只有一个布局文件,widget_item.xml,因为所有的项目都使用相同的布局。但WeatherListWidge示例有两个布局文件:dark_widget_item.xmllight_widget_item.xml

带集合的应用程序窗口小部件的AppWidgetProvider

与常规的应用程序窗口小部件一样,您的AppWidgetProvider子类中的大部分代码通常都会运行onUpdate()。在创建具有集合的应用程序窗口小部件时,您对onUpdate()的实现的主要区别在于您必须调用setRemoteAdapter()。这告诉集合视图从哪里得到它的数据。然后,RemoteViewsService可以返回您实现的RemoteViewsFactory,并且窗口小部件可以提供适当的数据。当你调用这个方法的时候,你必须传递一个指向你实现的RemoteViewsService的Intent,以及指定要更新的应用程序窗口小部件的ID。

例如,一下是StackView窗口小部件示例如何实现onUpdate()回调方法,以将RemoteViewsService设置为应用程序窗口小部件集合的远程适配器:

public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
    // update each of the 应用程序窗口小部件s 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 应用程序窗口小部件 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 应用程序窗口小部件 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 应用程序窗口小部件...
        //

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

RemoteViewsService类

持久化数据

您不能依靠您的服务的单个实例或其包含的任何数据来实现持久化。因此,您不应该在您的RemoteViewsService中存储任何数据(除非它是静态的)。如果您希望您的应用程序窗口小部件的数据实现持久化,那么最好的方法是使用ContentProvider,其数据会在整个流程生命周期之外持续存在。

如上所述,您的RemoteViewsService子类提供了RemoteViewsFactory用来填充远程集合视图。

具体来说,您需要执行这些步骤:

  1. 子类RemoteViewsService. RemoteViewsService是一个远程适配器可以通过其请求RemoteViews的服务。
  2. 在您的RemoteViewsService子类中,包含一个实现了RemoteViewsFactory接口的类。RemoteViewsFactory是一个远程集合视图(例如ListViewGridView等等)与该视图的基础数据之间的适配器的接口。您的实现负责为每个数据集中的项目创建一个RemoteViews对象。这个接口是Adapter的简单封装。

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

RemoteViewsFactory接口

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

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

在首次创建您的工厂类时系统会调用onCreate()方法。这是您设置数据源的连接和/或游标的地方。例如,StackView窗口小部件示例使用onCreate()来初始化一组WidgetItem对象。当您的应用程序窗口小部件处于活动状态时,系统将使用它们在数组中的索引位置访问这些对象,并显示它们包含的文本。

下面是StackView窗口小部件示例中的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对象。以下是StackView窗口小部件示例中的RemoteViewsFactory实现的摘录:

public RemoteViews getViewAt(int position) {

    // Construct a remote views item based on the 应用程序窗口小部件 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;
}

将行为添加到单个项目

以上部分向您展示了如何将您的数据绑定到您的应用程序窗口小部件2集合。但是如果你想添加动态行为到你的集合视图中的单个项目呢?

使用AppWidgetProvider类所述,通常使用setOnClickPendingIntent()来设置对象的点击行为-例如导致按钮启动一个Activity。但是,这种方法在单个集合项目中的子视图中是不允许的(为了说明,您可以在Gmail应用程序小部件中使用setOnClickPendingIntent()设置全局按钮来启动应用程序,而不是单个列表项目)。相反,要将点击行为添加到集合中的单个项目,您可以使用setOnClickFillInIntent()。这需要为您的集合视图设置PendingIntent模板,然后通过您的RemoteViewsFactory为集合中的每个项目设置填充Intent。

本节使用StackView窗口小部件示例来描述如何将行为添加到单个项目。在StackView窗口小部件示例中,如果用户触摸顶部视图,则应用程序窗口小部件将显示Toast消息“触摸视图n”,其中n是触摸视图的索引(位置)。一下是它的工作原理:

  • StackWidgetProvider(一个AppWidgetProvider的子类)创建一个带有叫TOAST_ACTION的自定义动作的PendingIntent。
  • 当用户触摸视图时,Intent被触发并且广播TOAST_ACTION
  • 这个广播被StackWidgetProvideronReceive()方法拦截,并且应用程序窗口小部件显示被触摸视图的Toast消息。集合项目的数据由RemoteViewsFactory通过RemoteViewsService提供。

注意:StackView窗口小部件示例使用了一个广播,但通常一个应用程序小窗口部件只会在这种情况下启动一个activity。

设置PendingIntent模板

StackWidgetProviderAppWidgetProvider 的子类)建立一个PendingIntent。集合中的单个项目不能设置它们自己的PendingIntent。相反,集合作为一个整体建立一个PendingIntent模板,并且各个项目逐项设置了一个填充Intent来创建独特的行为。

此类还接收用户触摸视图时发送的广播。在它的onReceive() 方法中处理这事件。如果Intent的动作是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";

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks to see whether the intent's action is TOAST_ACTION. If it is, the 应用程序窗口小部件
    // 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 应用程序窗口小部件s 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必须为集合中的每个项目设置一个填充意图。这使得区分给定项目的单个点击动作成为可能。然后将填充意图与PendingIntent模板组合,以确定在单击项目时将执行的最终意图。

您的RemoteViewsFactory必须为集合中的每个项目设置一个填充Intent。这样才能区分给定项目的单个点击动作。然后将填充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 应用程序窗口小部件 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 应用程序窗口小部件 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;
        }
    ...
    }

保持集合数据是最新的

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

使用集合的应用程序窗口小部件的一个功能是能够为用户提供最新的内容。例如,考虑Android 3.0 Gmail应用程序窗口小部件,它为用户提供收件箱的快照。为了使这成为可能,您需要触发您的RemoteViewsFactory和收集视图来获取和显示新的数据。您可以通过调用AppWidgetManager中的notifyAppWidgetViewDataChanged()方法来实现这一点。这个调用之后会回调您的RemoteViewsFactoryonDataSetChanged()方法,这使您有机会获取任何新的数据。请注意,您可以在onDataSetChanged()回调中同步执行过程密集型操作。您可以放心,这个调用将在从RemoteViewsFactory提取元数据或者视图数据之前完成。另外,您可以在getViewAt()方法中执行过程密集型操作。如果该调用需要很长的时间,装载视图(由RemoteViewsFactorygetLoadingView()方法指定)将被显示在集合视图,直到它返回相应的位置。

英文原文链接:https://developer.android.com/guide/topics/appwidgets/index.html

发布了30 篇原创文章 · 获赞 60 · 访问量 26万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览