使用集合小部件

集合小部件专门用于显示许多相同类型元素,例如来自图库应用的图片集合、来自新闻应用的文章集合或来自通信应用的消息集合。集合小部件通常侧重于两个用例:浏览集合和打开集合中的元素到其详细信息视图。集合小部件可以垂直滚动。

这些小部件使用 RemoteViewsService 来显示由远程数据支持的集合,例如来自 内容提供者 的数据。小部件使用以下视图类型之一来呈现数据,这些类型被称为集合视图

ListView
以垂直滚动列表显示项目的视图。
GridView
以二维滚动网格显示项目的视图。
StackView
堆叠式卡片视图,类似于转盘,用户可以将前一张卡片向上或向下翻转以分别查看前一张或下一张卡片。
AdapterViewFlipper
基于适配器的简单 ViewAnimator,可以在两个或多个视图之间进行动画切换。一次只显示一个子视图。

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

由于这些集合视图由适配器支持,因此 Android 框架必须包含额外的架构来支持它们在小部件中的使用。在小部件的上下文中,AdapterRemoteViewsFactory 替换,它是一个围绕 Adapter 接口的薄包装器。当请求集合中的特定项目时,RemoteViewsFactory 会创建并返回作为 RemoteViews 对象的集合项目。要将集合视图包含在您的 widget 中,请实现 RemoteViewsServiceRemoteViewsFactory

RemoteViewsService 是一种服务,允许远程适配器请求 RemoteViews 对象。RemoteViewsFactory 是集合视图(例如 ListViewGridViewStackView)与该视图的底层数据之间的适配器的接口。在您的实现中,负责为数据集中的每个项目创建一个 RemoteViews 对象。此接口是 Adapter 的一个薄包装器。

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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.

}

Kotlin

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.

}

Java

示例应用程序

本节中的代码摘录也来自 StackWidget 示例

图 1. StackWidget

  • 此示例包含一个包含十个视图的堆栈,这些视图显示值 0 到 9。示例小部件具有以下主要行为

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

  • 在没有任何用户交互的情况下,小部件会自动依次遍历其视图,就像幻灯片一样。这是由于 res/xml/stackwidgetinfo.xml 文件中的设置 android:autoAdvanceViewId="@id/stack_view"。此设置适用于视图 ID,在本例中是堆栈视图的视图 ID。

如果用户触摸顶部视图,小部件将显示 Toast 消息“触摸视图 n”,其中 n 是触摸视图的索引(位置)。有关如何实现行为的更多讨论,请参阅 向单个项目添加行为 部分。

使用集合实现小部件

要使用集合实现小部件,请按照 实现任何小部件 的步骤操作,然后执行一些额外的步骤:修改清单、将集合视图添加到小部件布局,并修改您的 AppWidgetProvider 子类。

使用集合的小部件的清单

除了在 在清单中声明小部件 中列出的要求之外,您还需要使使用集合的小部件能够绑定到您的 RemoteViewsService。通过在您的清单文件中使用权限 BIND_REMOTEVIEWS 声明该服务来实现此目的。这可以防止其他应用程序随意访问您的小部件数据。

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

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

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

使用集合的小部件的布局

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

小部件布局 XML 文件的主要要求是它包含以下集合视图之一:ListViewGridViewStackViewAdapterViewFlipper。以下是 StackWidget 示例widget_layout.xml 文件

请注意,空视图必须是集合视图的同级元素,空视图代表空状态。

除了整个小部件的布局文件之外,还要创建一个定义集合中每个项目的布局的另一个布局文件,例如,一组书籍中每本书的布局。StackWidget 示例只有一个项目布局文件 widget_item.xml,因为所有项目都使用相同的布局。

使用集合的 AppWidgetProvider 类

与普通小部件一样,您在 AppWidgetProvider 子类中的大部分代码通常放在 onUpdate() 中。在创建使用集合的小部件时,您在实现 onUpdate() 时,主要区别在于您必须调用 setRemoteAdapter()。这会告诉集合视图从哪里获取其数据。然后,RemoteViewsService 可以返回您对 RemoteViewsFactory 的实现,小部件可以提供适当的数据。当您调用此方法时,请传递一个指向您对 RemoteViewsService 的实现的意图和指定要更新的小部件的 widget ID。

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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

Kotlin

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

例如,以下是如何 StackWidget 示例实现 onUpdate() 回调方法,以将 RemoteViewsService 设置为小部件集合的远程适配器

持久化数据

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

  1. 具体来说,请执行以下步骤

  2. RemoteViewsService 进行子类化。 RemoteViewsService 是远程适配器可以请求 RemoteViews 的服务。

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

RemoteViewsService 实现的主要内容是其 RemoteViewsFactory,将在下一节中介绍。

RemoteViewsFactory 接口

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

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

系统在第一次创建工厂时调用 onCreate()。这是您设置与数据源的任何连接或游标的地方。例如,StackWidget 示例使用 onCreate() 初始化 WidgetItem 对象的数组。当您的小部件处于活动状态时,系统会使用其在数组中的索引位置访问这些对象,并显示其包含的文本。

以下是 StackWidget 示例的 RemoteViewsFactory 实现中显示部分 onCreate() 方法的摘录

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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

Kotlin

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() 返回一个 RemoteViews 对象,该对象对应于数据集指定 position 处的数据。以下是 StackWidget 示例的 RemoteViewsFactory 实现中的摘录

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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

Kotlin

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

向单个项目添加行为

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

使用 onUpdate() 类处理事件 中所述,通常使用 setOnClickPendingIntent() 设置对象的点击行为,例如使按钮启动 Activity。但是,此方法不允许用于单个集合项中的子视图。例如,您可以使用 setOnClickPendingIntent() 为 Gmail 小部件设置一个全局按钮,该按钮用于启动应用程序,例如,但不适用于各个列表项。

相反,要向集合中的单个项目添加点击行为,请使用 setOnClickFillInIntent()。这需要为您的集合视图设置一个待定意图模板,然后通过您的 RemoteViewsFactory 在集合中的每个项目上设置一个填充意图。

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

  • StackWidgetProvider(一个 AppWidgetProvider 子类)使用名为 TOAST_ACTION 的自定义操作创建一个待定意图。

  • 当用户触摸视图时,意图触发并广播 TOAST_ACTION

  • 此广播由 StackWidgetProvider 类的 onReceive() 方法拦截,小部件显示所触摸视图的 Toast 消息。集合项的数据由 RemoteViewsFactory 通过 RemoteViewsService 提供。

设置待定意图模板

StackWidgetProviderAppWidgetProvider 子类)设置待定意图。集合的各个项目无法设置自己的待定意图。相反,整个集合会设置一个待定意图模板,各个项目会设置填充意图,以逐个项目地创建唯一的行为。

此类还接收用户触摸视图时发送的广播。它在其 onReceive() 方法中处理此事件。如果意图的操作是 TOAST_ACTION,则小部件会显示当前视图的 Toast 消息。

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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

Kotlin

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

设置填充意图

您的 RemoteViewsFactory 必须在集合中的每个项目上设置填充意图。这使得能够区分给定项目的单个点击操作。然后将填充意图与 PendingIntent 模板组合起来,以确定在点击该项目时执行的最终意图。

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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

Kotlin

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() 方法指定),直到该调用返回为止,它才会显示在集合视图的相应位置。

使用 RemoteCollectionItems 直接传递集合

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

除了使填充适配器变得更容易之外,此方法还消除了用户向下滚动列表以显示新项目时填充新项目所产生的延迟。只要您的集合项集相对较小,此设置适配器的方法就是首选方法。但是,例如,如果您的集合包含大量传递到 setImageViewBitmapBitmaps,则此方法不适用。

如果集合不使用恒定的布局集,即,如果某些项目仅有时存在,请使用 setViewTypeCount 指定集合可以包含的唯一布局的最大数量。这使适配器可以在对应用程序小部件的更新之间重复使用。

以下是如何实现简化 RemoteViews 集合的示例。

StackWidget 示例 中,以下是如何实现此服务和接口的样板代码

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()
)

Kotlin

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