使用集合微件

集合 widget 专门用于显示同一类型的多个元素,例如图库应用中的图片集合、新闻应用中的文章或通讯应用中的邮件。集合 widget 通常侧重于以下两种用例:浏览集合,以及将集合中的元素打开到其详情视图。集合微件可以垂直滚动。

这些微件使用 RemoteViewsService 来显示由远程数据(如来自内容提供程序的数据)支持的集合。该微件使用以下某种视图类型(称为集合视图)呈现数据:

ListView
一种在垂直滚动列表中显示项目的视图。
GridView
一种在二维滚动网格中显示项目的视图。
StackView
一种堆叠式卡片视图(有点像名片盒),用户可以分别向上或向下翻动前面的卡片来查看上一张或下一张卡片。
AdapterViewFlipper
一种由适配器支持的简单 ViewAnimator,可以在两个或更多视图之间呈现动画效果。一次只显示一个子级。

由于这些集合视图显示由远程数据支持的集合,因此它们使用 Adapter 将其界面绑定到其数据。Adapter 将一组数据中的各个项目绑定到各个 View 对象。

由于这些集合视图由适配器支持,因此 Android 框架必须包含额外的架构来支持它们在微件中的使用。在微件的上下文中,AdapterRemoteViewsFactory 取代,后者是 Adapter 接口的瘦封装容器。请求集合中的特定项目时,RemoteViewsFactory 会为集合创建相应项目并将其作为 RemoteViews 对象返回。如需在微件中添加集合视图,请实现 RemoteViewsServiceRemoteViewsFactory

RemoteViewsService 是一项服务,可让远程适配器请求 RemoteViews 对象。RemoteViewsFactory 是集合视图(例如 ListViewGridViewStackView)与该视图的底层数据之间的适配器的接口。下面是用于实现此服务和接口的样板代码的示例(来自 StackWidget 示例):

Kotlin

class StackWidgetService : RemoteViewsService() {

    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
        return StackRemoteViewsFactory(this.applicationContext, intent)
    }
}

class StackRemoteViewsFactory(
        private val context: Context,
        intent: Intent
) : RemoteViewsService.RemoteViewsFactory {

// See the RemoteViewsFactory API reference for the full list of methods to
// implement.

}

Java

public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

// See the RemoteViewsFactory API reference for the full list of methods to
// implement.

}

示例应用

本部分中的代码段也摘录自 StackWidget 示例

图 1. StackWidget

此示例由一个包含 10 个视图的堆栈组成,这些视图显示值 0 到 9。该示例微件具有以下主要行为:

  • 用户可以垂直滑动该微件中的顶部视图以显示下一个或上一个视图。这是内置的 StackView 行为。

  • 如果没有任何用户交互,则该微件会自动按顺序显示其视图,就像播放幻灯片一样。这是因为在 res/xml/stackwidgetinfo.xml 文件中设置了 android:autoAdvanceViewId="@id/stack_view"。此设置适用于视图 ID,在本例中为堆栈视图的视图 ID。

  • 如果用户触摸顶部视图,该微件会显示 Toast 消息“Touched view n”,其中 n 是触摸的视图的索引(位置)。如需详细了解如何实现行为,请参阅向各个项目添加行为部分。

实现包含集合的 widget

如需实现包含集合的微件,请按照实现任何微件的步骤操作,然后再执行一些额外的步骤:修改清单、将集合视图添加到微件布局,以及修改 AppWidgetProvider 子类。

包含集合的微件的清单

除了在清单中声明 widget 中列出的要求之外,您还需要使包含集合的 widget 能够绑定到 RemoteViewsService。为此,请在清单文件中声明具有 BIND_REMOTEVIEWS 权限的服务。这样可防止其他应用自由访问您的微件的数据。

例如,在创建使用 RemoteViewsService 填充集合视图的 widget 时,清单条目可能如下所示:

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

在此示例中,android:name="MyWidgetService" 引用您的 RemoteViewsService 子类。

包含集合的 widget 的布局

对微件布局 XML 文件的主要要求是它必须包含某个集合视图:ListViewGridViewStackViewAdapterViewFlipper。以下是 StackWidget 示例widget_layout.xml 文件:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <StackView
        android:id="@+id/stack_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:loopViews="true" />
    <TextView
        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>

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

除了整个微件的布局文件之外,您还需要再创建一个布局文件,用来定义集合中每个项目的布局(例如,一套图书中每本图书的布局)。StackWidget 示例只有一个项布局文件 widget_item.xml,因为所有项使用同一布局。

包含集合的 widget 的 AppWidgetProvider 类

与常规微件一样,AppWidgetProvider 子类中的大部分代码通常都在 onUpdate() 中。在创建包含集合的微件时,您的 onUpdate() 实现的主要区别在于,您必须调用 setRemoteAdapter()。这样将告知集合视图要从何处获取其数据。然后,RemoteViewsService 可以返回您的 RemoteViewsFactory 实现,并且微件可以提供适当的数据。调用此方法时,请传递指向您的 RemoteViewsService 实现的 intent,以及指定要更新的微件的微件 ID。

例如,以下代码段说明了 StackWidget 示例如何实现 onUpdate() 回调方法以将 RemoteViewsService 设为微件集合的远程适配器:

Kotlin

override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
) {
    // Update each of the widgets with the remote adapter.
    appWidgetIds.forEach { appWidgetId ->

        // Set up the intent that starts the StackViewService, which
        // provides the views for this collection.
        val intent = Intent(context, StackWidgetService::class.java).apply {
            // Add the widget ID to the intent extras.
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
        }
        // Instantiate the RemoteViews object for the widget layout.
        val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
            // 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.
            setRemoteAdapter(R.id.stack_view, intent)

            // The empty view is displayed when the collection has no items.
            // It must be in the same layout used to instantiate the
            // RemoteViews object.
            setEmptyView(R.id.stack_view, R.id.empty_view)
        }

        // Do additional processing specific to this widget.

        appWidgetManager.updateAppWidget(appWidgetId, views)
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds)
}

Java

public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    // Update each of the widgets with the remote adapter.
    for (int i = 0; i < appWidgetIds.length; ++i) {

        // Set up the intent that starts the StackViewService, which
        // provides the views for this collection.
        Intent intent = new Intent(context, StackWidgetService.class);
        // Add the 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 widget layout.
        RemoteViews views = 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.
        views.setRemoteAdapter(R.id.stack_view, intent);

        // The empty view is displayed when the collection has no items.
        // It must be in the same layout used to instantiate the RemoteViews
        // object.
        views.setEmptyView(R.id.stack_view, R.id.empty_view);

        // Do additional processing specific to this widget.

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

保留数据

如本页所述,RemoteViewsService 子类提供用于填充远程集合视图的 RemoteViewsFactory

具体而言,请执行以下步骤:

  1. RemoteViewsService 子类。RemoteViewsService 是一项服务,远程适配器可以通过它来请求 RemoteViews

  2. 在您的 RemoteViewsService 子类中,添加一个实现 RemoteViewsFactory 接口的类。RemoteViewsFactory 是远程集合视图(如 ListViewGridViewStackView)与该视图的底层数据之间的适配器的接口。您的实现负责为数据集中的每个项目创建一个 RemoteViews 对象。此接口是 Adapter 的瘦封装容器。

您不能依赖于服务的单个实例,也不能保留它包含的任何数据。请勿在 RemoteViewsService 中存储数据,除非它是静态的。如果您要保留微件的数据,最好的方法是使用 ContentProvider,它的数据在进程生命周期过后持续存在。例如,杂货店微件可以将每个购物清单项的状态存储在持久性位置(例如 SQL 数据库)中。

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

RemoteViewsFactory 接口

实现 RemoteViewsFactory 接口的自定义类可以为微件包含的集合中的项目提供数据。为此,它会将微件项 XML 布局文件与数据源相结合。此数据源可以是任何来源,从数据库到简单的数组均可。在 StackWidget 示例中,数据源是 WidgetItems 的数组。RemoteViewsFactory 充当将数据粘附到远程集合视图的适配器。

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

首次创建 RemoteViewsFactory 接口时,系统会调用 onCreate()。您可以在此方法中设置指向数据源的任何连接或游标。例如,StackWidget 示例使用 onCreate() 来初始化 WidgetItem 对象的数组。当您的微件处于活动状态时,系统会使用这些对象在数组中的索引位置来对其进行访问,并且会显示它们包含的文本。

以下代码段摘录自 StackWidget 示例的 RemoteViewsFactory 实现,显示了 onCreate() 方法的某些部分:

Kotlin

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context
) : RemoteViewsService.RemoteViewsFactory {

    private lateinit var widgetItems: List<WidgetItem>

    override fun onCreate() {
        // In onCreate(), set up any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating content,
        // must be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds on this call results in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...
}

Java

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final int REMOTE_VIEW_COUNT = 10;
    private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();

    public void onCreate() {
        // In onCreate(), setup any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating content,
        // must be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds on this call results in an ANR.
        for (int i = 0; i < REMOTE_VIEW_COUNT; i++) {
            widgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }
...

RemoteViewsFactory 方法 getViewAt() 将返回与位于数据集中指定 position 的数据对应的 RemoteViews 对象。以下代码段摘录自 StackWidget 示例的 RemoteViewsFactory 实现:

Kotlin

override fun getViewAt(position: Int): RemoteViews {
    // Construct a remote views item based on the widget item XML file
    // and set the text based on the position.
    return RemoteViews(context.packageName, R.layout.widget_item).apply {
        setTextViewText(R.id.widget_item, widgetItems[position].text)
    }
}

Java

public RemoteViews getViewAt(int position) {
    // Construct a remote views item based on the widget item XML file
    // and set the text based on the position.
    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_item);
    views.setTextViewText(R.id.widget_item, widgetItems.get(position).text);
    return views;
}

向各个项目添加行为

前面的部分介绍了如何将数据绑定到微件集合。但是,如果您要向集合视图中的各个项目添加动态行为,该怎么办呢?

使用 onUpdate() 类处理事件中所述,您通常使用 setOnClickPendingIntent() 来设置对象的点击行为 - 例如,让按钮启动 Activity。但是,不允许对各个集合项中的子视图使用此方法。例如,您可以使用 setOnClickPendingIntent() 在 Gmail 微件中设置一个用来启动应用的全局按钮,但不能在各个列表项上进行设置。

如需向集合中的各个项目添加点击行为,请改用 setOnClickFillInIntent()。这需要为集合视图设置待定 intent 模板,然后通过 RemoteViewsFactory 在集合中的每个项目上设置填充 intent。

本部分通过 StackWidget 示例来说明如何向各个项目添加行为。在 StackWidget 示例中,如果用户触摸顶部视图,该微件会显示 Toast 消息“Touched view n”,其中 n 是触摸的视图的索引(位置)。其工作原理如下:

  • StackWidgetProviderAppWidgetProvider 子类)会创建一个待处理 intent,该 intent 具有一项名为 TOAST_ACTION 的自定义操作。

  • 当用户触摸视图时,系统会触发 intent,并且它会广播 TOAST_ACTION

  • 此广播会被 StackWidgetProvider 类的 onReceive() 方法拦截,并且微件会针对触摸的视图显示 Toast 消息。集合项的数据由 RemoteViewsFactory 通过 RemoteViewsService 提供。

设置待处理 intent 模板

StackWidgetProviderAppWidgetProvider 子类)会设置一个待定 intent。集合中的各个项目无法设置它们自己的待处理 intent。而是整个集合设置一个待定 intent 模板,并且各个项目设置填充 intent 来逐项创建唯一的行为。

此类还会接收用户触摸视图时发送的广播。它在自己的 onReceive() 方法中处理此事件。如果 intent 的操作为 TOAST_ACTION,则微件会针对当前视图显示 Toast 消息。

Kotlin

const val TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION"
const val EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM"

class StackWidgetProvider : AppWidgetProvider() {

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks whether the intent's action is TOAST_ACTION. If it is, the
    // widget displays a Toast message for the current item.
    override fun onReceive(context: Context, intent: Intent) {
        val mgr: AppWidgetManager = AppWidgetManager.getInstance(context)
        if (intent.action == TOAST_ACTION) {
            val appWidgetId: Int = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID
            )
            // EXTRA_ITEM represents a custom value provided by the Intent
            // passed to the setOnClickFillInIntent() method to indicate the
            // position of the clicked item. See StackRemoteViewsFactory in
            // Set the fill-in Intent for details.
            val viewIndex: Int = intent.getIntExtra(EXTRA_ITEM, 0)
            Toast.makeText(context, "Touched view $viewIndex", Toast.LENGTH_SHORT).show()
        }
        super.onReceive(context, intent)
    }

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Update each of the widgets with the remote adapter.
        appWidgetIds.forEach { appWidgetId ->

            // Sets up the intent that points to the StackViewService that
            // provides the views for this collection.
            val intent = Intent(context, StackWidgetService::class.java).apply {
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                // When intents are compared, the extras are ignored, so embed
                // the extra sinto the data so that the extras are not ignored.
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
            }
            val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply {
                setRemoteAdapter(R.id.stack_view, intent)

                // The empty view is displayed when the collection has no items.
                // It must be a sibling of the collection view.
                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 can't 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.
            val toastPendingIntent: PendingIntent = Intent(
                    context,
                    StackWidgetProvider::class.java
            ).run {
                // Set the action for the intent.
                // When the user touches a particular view, it has the effect of
                // broadcasting TOAST_ACTION.
                action = TOAST_ACTION
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))

                PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT)
            }
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)

            appWidgetManager.updateAppWidget(appWidgetId, rv)
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
}

Java

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 whether the intent's action is TOAST_ACTION. If it is, the
    // 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);
            // EXTRA_ITEM represents a custom value provided by the Intent
            // passed to the setOnClickFillInIntent() method to indicate the
            // position of the clicked item. See StackRemoteViewsFactory in
            // Set the fill-in Intent for details.
            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 widgets with the remote adapter.
        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Sets up the intent that points to the StackViewService that
            // provides 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 embed
            // the extras into the data so that the extras are not
            // 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
            // must 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 can't 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 has 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。这样就可以区分给定项目的点击时的各项操作。填充 intent 随后与 PendingIntent 模板相结合,以确定在点按相应项目时要执行的最终 intent。

Kotlin

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context,
        intent: Intent
) : RemoteViewsService.RemoteViewsFactory {

    private lateinit var widgetItems: List<WidgetItem>
    private val appWidgetId: Int = intent.getIntExtra(
            AppWidgetManager.EXTRA_APPWIDGET_ID,
            AppWidgetManager.INVALID_APPWIDGET_ID
    )

    override fun onCreate() {
        // In onCreate(), set up any connections or cursors to your data source.
        // Heavy lifting, such as downloading or creating content, must be
        // deferred to onDataSetChanged() or getViewAt(). Taking more than 20
        // seconds on this call results in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...

    override fun getViewAt(position: Int): RemoteViews {
        // Construct a remote views item based on the widget item XML file
        // and set the text based on the position.
        return RemoteViews(context.packageName, R.layout.widget_item).apply {
            setTextViewText(R.id.widget_item, widgetItems[position].text)

            // Set a fill-intent to fill in the pending intent template.
            // that is set on the collection view in StackWidgetProvider.
            val fillInIntent = Intent().apply {
                Bundle().also { extras ->
                    extras.putInt(EXTRA_ITEM, position)
                    putExtras(extras)
                }
            }
            // Make it possible to distinguish the individual on-click
            // action of a given item.
            setOnClickFillInIntent(R.id.widget_item, fillInIntent)
            ...
        }
    }
    ...
}

Java

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 count = 10;
    private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();
    private Context context;
    private int appWidgetId;

    public StackRemoteViewsFactory(Context context, Intent intent) {
        this.context = context;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }

    // Initialize the data set.
    public void onCreate() {
        // In onCreate(), set up any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating
        // content, must be deferred to onDataSetChanged() or
        // getViewAt(). Taking more than 20 seconds on this call results
        // in an ANR.
        for (int i = 0; i < count; i++) {
            widgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }

    // Given the position (index) of a WidgetItem in the array, use the
    // item's text value in combination with the widget item XML file to
    // construct a RemoteViews object.
    public RemoteViews getViewAt(int position) {
        // Position always ranges from 0 to getCount() - 1.

        // Construct a RemoteViews item based on the widget item XML
        // file and set the text based on the position.
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, widgetItems.get(position).text);

        // Set a fill-intent 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;
    }
    ...
}

使合集数据保持最新

图 2 说明了使用集合的微件中的更新流程。图中显示了微件代码如何与 RemoteViewsFactory 交互,以及您如何触发更新:

图 2. 更新期间与 RemoteViewsFactory 的交互。

使用集合的微件可以为用户提供最新内容。例如,Gmail 微件可为用户提供收件箱的快照。要做到这一点,请触发 RemoteViewsFactory 和集合视图以提取和显示新数据。

为此,请使用 AppWidgetManager 调用 notifyAppWidgetViewDataChanged()。此调用会导致对 RemoteViewsFactory 对象的 onDataSetChanged() 方法进行回调,从而让您有机会获取任何新数据。

您可以在 onDataSetChanged() 回调中同步执行处理密集型操作。可以保证您会在从 RemoteViewsFactory 获取元数据或视图数据之前完成此调用。您还可以在 getViewAt() 方法中执行处理密集型操作。如果此调用需要很长时间,则正在加载的视图(由 RemoteViewsFactory 对象的 getLoadingView() 方法指定)会显示在集合视图的相应位置,直到它返回结果。

使用 RemoteCollectionItem 直接传递集合

Android 12(API 级别 31)添加了 setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) 方法,可让您的应用在填充集合视图时直接传递集合。如果您使用此方法设置适配器,则无需实现 RemoteViewsFactory,也不需要调用 notifyAppWidgetViewDataChanged()

除了让您更轻松地填充适配器之外,这种方法还消除了在用户向下滚动列表以显示新项时填充新项的延迟时间。只要您的一组集合项相对较少,这种设置适配器的方法就是首选。不过,例如,如果集合包含要传递给 setImageViewBitmap 的大量 Bitmaps,此方法就行不通。

如果集合未使用一组恒定的布局(换句话说,如果某些项只是有时存在),请使用 setViewTypeCount 指定集合可以包含的独特布局的数量上限。这样,您就可以在应用 widget 更新时重复使用适配器。

以下示例说明了如何实现简化的 RemoteViews 集合。

Kotlin

val itemLayouts = listOf(
        R.layout.item_type_1,
        R.layout.item_type_2,
        ...
)

remoteView.setRemoteAdapter(
        R.id.list_view,
        RemoteViews.RemoteCollectionItems.Builder()
            .addItem(/* id= */ ID_1, RemoteViews(context.packageName, R.layout.item_type_1))
            .addItem(/* id= */ ID_2, RemoteViews(context.packageName, R.layout.item_type_2))
            ...
            .setViewTypeCount(itemLayouts.count())
            .build()
)

Java

List<Integer> itemLayouts = Arrays.asList(
    R.layout.item_type_1,
    R.layout.item_type_2,
    ...
);

remoteView.setRemoteAdapter(
    R.id.list_view,
    new RemoteViews.RemoteCollectionItems.Builder()
        .addItem(/* id= */ ID_1, new RemoteViews(context.getPackageName(), R.layout.item_type_1))
        .addItem(/* id= */ ID_2, new RemoteViews(context.getPackageName(), R.layout.item_type_2))
        ...
        .setViewTypeCount(itemLayouts.size())
        .build()
);