使用珍藏內容小工具

集合小工具專門顯示相同類型的許多元素,例如圖片庫應用程式的相片集合、新聞應用程式的文章或通訊應用程式的訊息。收集小工具通常著重於兩種用途:瀏覽集合,以及開啟集合元素到詳細資料檢視畫面。集合小工具可以垂直捲動。

這些小工具會使用 RemoteViewsService 顯示遠端資料支援的集合,例如內容供應器。小工具會使用下列其中一種檢視畫面類型 (稱為「集合檢視畫面」) 顯示資料:

ListView
以垂直捲動清單顯示項目的檢視畫面。
GridView
以二維捲動格線顯示項目的檢視畫面。
StackView
堆疊的資訊卡檢視畫面是一種 Rolodex,使用者只要在正面資訊卡上下滑動,就能分別查看上一張或下一張資訊卡。
AdapterViewFlipper
採用轉接程式支援的簡易 ViewAnimator,以動畫形式在兩個以上檢視畫面之間建立動畫效果。系統一次只會顯示一名兒童。

這些集合檢視畫面會顯示遠端資料的集合,因此會使用 Adapter 將使用者介面繫結至資料。Adapter 會將一組資料中的個別項目繫結至個別 View 物件。

此外,由於這些集合檢視畫面是由轉接程式支援,因此 Android 架構必須包含額外架構,才能在小工具中使用。在小工具的結構定義中,Adapter 會替換為 RemoteViewsFactory,這是 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

這個範例含有十個檢視畫面的堆疊,顯示值為零到九。範例小工具有以下主要行為:

  • 使用者可以在小工具中垂直快速滑過頂端檢視畫面,以顯示下一個或上一個檢視畫面。這是內建的 StackView 行為。

  • 小工具會在沒有任何使用者互動的情況下,自動依序前進,就像投影播放一樣。這是由於 res/xml/stackwidgetinfo.xml 檔案中的 android:autoAdvanceViewId="@id/stack_view" 設定所致。這項設定適用於檢視畫面 ID,在本例中為堆疊檢視畫面的檢視畫面 ID。

  • 如果使用者輕觸頂端檢視畫面,小工具會顯示 Toast 訊息「Touched view n」,其中「n」代表輕觸的檢視畫面的索引 (位置)。如要進一步瞭解如何實作行為,請參閱「為個別項目新增行為」一節。

透過集合實作小工具

如要實作包含集合的小工具,請按照實作任何小工具的程序操作,隨後再完成幾個步驟:修改資訊清單、在小工具版面配置中加入集合檢視畫面,以及修改 AppWidgetProvider 子類別。

內含集合的小工具資訊清單

除了「在資訊清單中宣告小工具」中列出的規定外,您必須允許含有集合的小工具繫結至 RemoteViewsService。方法是在資訊清單檔案中使用 BIND_REMOTEVIEWS 權限宣告服務。這會防止其他應用程式自由存取小工具的資料。

舉例來說,建立使用 RemoteViewsService 填入集合檢視畫面的小工具時,資訊清單項目可能如下所示:

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

在此範例中,android:name="MyWidgetService" 是指 RemoteViewsService 的子類別。

含有集合的小工具版面配置

小工具版面配置 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

適用於含有集合的小工具的 AppWidgetProvider 類別

與一般小工具一樣,AppWidgetProvider 子類別中的大量程式碼通常位於 onUpdate()。建立包含集合的小工具時,onUpdate() 實作的主要差異在於您必須呼叫 setRemoteAdapter()。用於告知集合檢視畫面要在哪裡取得資料。接著,RemoteViewsService 可以傳回 RemoteViewsFactory 實作,讓小工具提供適當的資料。呼叫此方法時,請傳遞指向您 RemoteViewsService 實作的意圖,以及指定要更新小工具的小工具 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. 子類別 RemoteViewsServiceRemoteViewsService 是遠端轉接程式可要求 RemoteViews 的服務。

  2. RemoteViewsService 子類別中,納入實作 RemoteViewsFactory 介面的類別。RemoteViewsFactory 是遠端集合檢視畫面 (例如 ListViewGridViewStackView) 與該檢視區塊的基礎資料之間的轉接程式介面。您的實作負責為資料集中的每個項目建立 RemoteViews 物件。這個介面是 Adapter 的精簡包裝函式。

您不能依賴服務的單一執行個體或其中任何資料來保留。除非資料是靜態資料,否則不要在 RemoteViewsService 中儲存資料。如果想保留小工具的資料,最佳做法是使用 ContentProvider,其資料在程序生命週期之外保留的資料。舉例來說,雜貨店小工具可以將每個雜貨清單項目的狀態儲存在永久位置 (例如 SQL 資料庫)。

RemoteViewsService 實作的主要內容為其 RemoteViewsFactory,如下一節所述。

RemoteViewsFactory 介面

實作 RemoteViewsFactory 介面的自訂類別會為小工具提供其集合中的項目資料。為此,程式碼會合併小工具項目的 XML 版面配置檔案和資料來源。這個資料來源可以是資料庫到簡易陣列中的任何內容。在 StackWidget 範例中,資料來源是 WidgetItems 的陣列。RemoteViewsFactory 功能可做為轉接器,將資料黏到遠端集合檢視畫面。

需要為 RemoteViewsFactory 子類別實作的兩個最重要的方法為 onCreate()getViewAt()

首次建立工廠時,系統會呼叫 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() 會傳回 RemoteViews 物件,對應資料集內指定 position 的資料。以下摘錄自 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(),為集合中的個別項目新增點擊行為。這表示您需要為集合檢視畫面設定待處理意圖範本,然後透過 RemoteViewsFactory 為集合中的每個項目設定填入意圖。

本節使用 StackWidget 範例,說明如何在個別項目中新增行為。在 StackWidget 範例中,如果使用者輕觸頂端檢視畫面,小工具會顯示 Toast 訊息「Touched view n」,其中「n」是觸控檢視區塊的索引 (位置)。這類廣告運作方式如下:

  • StackWidgetProvider (AppWidgetProvider 子類別) 會使用名為 TOAST_ACTION 的自訂動作建立待處理意圖。

  • 當使用者輕觸檢視畫面時,意圖會觸發並廣播 TOAST_ACTION

  • 這個廣播訊息會由 StackWidgetProvider 類別的 onReceive() 方法攔截,小工具也會顯示觸控檢視畫面的 Toast 訊息。集合項目的資料是由 RemoteViewsFactory 透過 RemoteViewsService 提供。

設定待處理意圖範本

StackWidgetProvider (AppWidgetProvider 子類別) 會設定待處理意圖。集合中的個別項目無法設定自己的待處理意圖。反之,整個集合會設定一個待處理意圖範本,然後由個別項目設定填入意圖,以針對個別項目建立不重複的行為。

這個類別也會接收使用者輕觸檢視畫面時傳送的廣播。其會在其 onReceive() 方法中處理這個事件。如果意圖的動作為 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);
    }
}

設定填入意圖

RemoteViewsFactory 必須針對集合中的每個項目設定填入意圖。如此一來,即可區分指定項目的個別點擊動作。接著,填入意圖會與 PendingIntent 範本結合,藉此判斷輕觸項目時執行的最終意圖。

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() 方法指定的載入檢視畫面會顯示在集合檢視畫面的對應位置,直到該呼叫傳回為止。

使用 RemoteCollectionItems 直接傳遞集合

Android 12 (API 級別 31) 新增了 setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) 方法,可讓應用程式在填入集合檢視畫面時直接傳遞集合。如果您使用這個方法設定轉接程式,就不需要實作 RemoteViewsFactory,也不需要呼叫 notifyAppWidgetViewDataChanged()

這個方法除了可以更輕鬆地填入轉接程式,使用者也可在向下捲動清單查看新項目時,減少填入新項目的延遲時間。只要集合項目組合相對較小,建議使用此方法設定轉接器。不過,如果集合包含多個 Bitmaps 傳遞至 setImageViewBitmap,此方法就無法正常運作。

如果集合未使用固定的版面配置組合 (也就是有些項目有時只存在),請使用 setViewTypeCount 指定集合可包含的獨特版面配置數量上限。如此一來,在應用程式小工具之間更新時,即可重複使用轉接程式。

以下舉例說明如何實作簡化的 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()
);