建立簡易小工具

應用程式小工具是一種小型的應用程式檢視畫面,可嵌入其他應用程式 (例如主畫面),並接收定期更新。這類檢視畫面在使用者介面中稱為「小工具」,您可以透過應用程式小工具供應商 (或「小工具供應商」) 發布小工具。容納其他小工具的應用程式元件稱為應用程式小工具主機 (或「小工具主機」)。圖 1 顯示範例音樂小工具:

音樂小工具範例
圖 1. 音樂小工具範例。

本文說明如何使用小工具供應商發布小工具。如要進一步瞭解如何建立自己的 AppWidgetHost 以代管應用程式小工具,請參閱「建構小工具主機」。

如要瞭解如何設計小工具,請參閱「應用程式小工具總覽」。

小工具元件

如要建立小工具,您必須具備下列基本元件:

AppWidgetProviderInfo 物件
說明小工具的中繼資料,例如小工具的版面配置、更新頻率和 AppWidgetProvider 類別。AppWidgetProviderInfo在 XML 中定義,詳情請參閱本文件。
AppWidgetProvider 類別
定義基本方法,透過小工具進行介面操作。透過這個小工具,當小工具更新、啟用、停用或刪除時,您就會收到廣播訊息。您可以在資訊清單中宣告 AppWidgetProvider,然後實作,如本文件所述。
查看版面配置
定義小工具的初始版面配置。版面配置是在 XML 中定義,如本文件所述。

圖 2 顯示這些元件如何融入整體的應用程式小工具處理流程。

應用程式小工具處理流程
圖 2. 應用程式小工具處理流程。

如果您的小工具需要使用者設定,請實作應用程式小工具設定活動。這項活動可讓使用者修改小工具設定,例如時鐘小工具的時區。

我們也建議進行下列改善:彈性的小工具版面配置其他強化功能進階小工具集合小工具建構小工具主機

宣告 AppWidgetProviderInfo XML

AppWidgetProviderInfo 物件會定義小工具的基本特質。使用單一 <appwidget-provider> 元素在 XML 資源檔案中定義 AppWidgetProviderInfo 物件,並儲存至專案的 res/xml/ 資料夾。

例如:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="120dp"
    android:updatePeriodMillis="86400000"
    android:description="@string/example_appwidget_description"
    android:previewLayout="@layout/example_appwidget_preview"
    android:initialLayout="@layout/example_loading_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>

小工具大小屬性

預設主畫面會根據已定義高度和寬度的儲存格格線,在視窗中放置小工具。在大部分的主畫面中,小工具只會採用格狀儲存格整數倍數的大小,例如兩個垂直排列的三個儲存格。

小工具大小屬性可讓您為小工具指定預設大小,並提供小工具的上下限。在這種情況下,小工具的預設大小就是小工具首次新增至主畫面時採用的大小。

下表說明與小工具大小相關的 <appwidget-provider> 屬性:

屬性和說明
targetCellWidthtargetCellHeight (Android 12)、minWidthminHeight
  • 從 Android 12 開始,targetCellWidthtargetCellHeight 屬性會在格線儲存格中指定小工具的預設大小。Android 11 以下版本「會」忽略這些屬性,如果主畫面不支援格線式版面配置,則可以忽略這些屬性。
  • minWidthminHeight 屬性可指定小工具的預設大小 (以 dp 為單位)。如果小工具的最小寬度或高度值與儲存格尺寸不符,則數值會無條件進位至最接近的儲存格大小。
建議您同時指定 targetCellWidthtargetCellHeight 以及 minWidthminHeight 等屬性組合,這樣使用者的裝置不支援 targetCellWidthtargetCellHeight 時,應用程式就可以改回使用 minWidthminHeight。如果支援,targetCellWidthtargetCellHeight 屬性的優先順序高於 minWidthminHeight 屬性。
minResizeWidthminResizeHeight 指定小工具的最小尺寸下限。這些值會指定小工具難以辨識或以其他方式無法使用的大小。使用這些屬性可讓使用者將小工具調整為小於預設小工具大小的大小。如果 minResizeWidth 屬性大於 minWidth,或未啟用水平調整大小功能,系統就會忽略該屬性。請參閱 resizeMode。同樣地,如果 minResizeHeight 屬性大於 minHeight,或未啟用垂直調整大小功能,系統就會忽略該屬性。
maxResizeWidthmaxResizeHeight 指定小工具的建議大小上限。如果這些值不是格線儲存格尺寸的倍數,系統會將這些值四捨五入至最接近的儲存格大小。如果 maxResizeWidth 屬性小於 minWidth 或未啟用水平調整大小功能,系統就會忽略該屬性。查看 resizeMode。同樣地,如果 maxResizeHeight 屬性大於 minHeight,或未啟用垂直調整大小功能,系統就會忽略該屬性。相關元素已在 Android 12 推出。
resizeMode 指定可調整小工具大小的規則。您可以使用這項屬性,讓主畫面小工具可水平、垂直或同時在兩軸上調整大小。使用者按住小工具即可顯示調整大小控點,然後拖曳水平或垂直控點來變更版面配置格線上的大小。resizeMode 屬性的值包括 horizontalverticalnone。如要將小工具宣告為可水平和垂直調整大小,請使用 horizontal|vertical

範例

為了說明上表中的屬性如何影響小工具大小,請使用下列規格:

  • 格線儲存格寬度為 30 dp,高度為 50 dp。
  • 您提供的屬性規格如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="80dp"
    android:minHeight="80dp"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:minResizeWidth="40dp"
    android:minResizeHeight="40dp"
    android:maxResizeWidth="120dp"
    android:maxResizeHeight="120dp"
    android:resizeMode="horizontal|vertical" />

從 Android 12 開始:

使用 targetCellWidthtargetCellHeight 屬性做為小工具的預設大小。

根據預設,小工具的大小為 2x2。小工具大小可調整為 2x1 或 4x3 以下。

Android 11 以下版本:

使用 minWidthminHeight 屬性計算小工具的預設大小。

預設寬度 = Math.ceil(80 / 30) = 3

預設高度 = Math.ceil(80 / 50) = 2

根據預設,小工具的大小為 3x2。小工具的大小可縮小至 2x1,也可調整為全螢幕。

其他小工具屬性

下表說明與小工具大小以外品質相關的 <appwidget-provider> 屬性。

屬性和說明
updatePeriodMillis 定義小工具架構透過呼叫 onUpdate() 回呼方法,向 AppWidgetProvider 要求更新的頻率。實際更新不保證會依照這個值即時執行,因此建議您盡可能不頻繁更新 (一小時不超過一次),以節省電池電力。 如需選擇適當更新期的完整注意事項清單,請參閱「對更新小工具內容的更新項目進行最佳化」。
initialLayout 指向定義小工具版面配置的版面配置資源。
configure 定義使用者新增小工具時啟動的活動,讓使用者設定小工具屬性。請參閱「允許使用者設定小工具」。從 Android 12 開始,應用程式可以略過初始設定。詳情請參閱「使用小工具的預設設定」。
description 指定小工具挑選器的顯示說明。相關元素已在 Android 12 推出。
previewLayout (Android 12) 和 previewImage (Android 11 以下版本)
  • 從 Android 12 開始,previewLayout 屬性會指定可擴充的預覽畫面,並以 XML 版面配置設為小工具預設大小的 XML 版面配置。在理想情況下,此屬性指定的版面配置 XML 與包含實際預設值的實際小工具相同。
  • 在 Android 11 以下版本中,previewImage 屬性會指定小工具設定完成後顯示的預覽畫面,供使用者選取應用程式小工具時看到。如果未提供,則使用者會看到應用程式的啟動器圖示。這個欄位對應 AndroidManifest.xml 檔案中 <receiver> 元素中的 android:previewImage 屬性。
注意:建議您同時指定 previewImagepreviewLayout 屬性,這樣一來,如果使用者的裝置不支援 previewLayout,應用程式就能改回使用 previewImage。詳情請參閱具備可擴充小工具預覽的回溯相容性
autoAdvanceViewId 指定小工具主機自動進階的小工具子檢視畫面的檢視畫面 ID。
widgetCategory 宣告小工具可以顯示在主畫面 (home_screen)、螢幕鎖定畫面 (keyguard) 或兩者皆顯示。如果是 Android 5.0 以上版本,則只有 home_screen 有效。
widgetFeatures 宣告小工具支援的功能。舉例來說,如果您希望小工具在使用者新增時採用預設設定,請同時指定 configuration_optionalreconfigurable 標記。這樣就不必在使用者新增小工具後啟動設定活動。之後使用者仍然可以重新設定小工具

使用 AppWidgetProvider 類別處理小工具廣播

AppWidgetProvider 類別會處理小工具廣播,並根據小工俱生命週期事件更新小工具。以下各節將說明如何在資訊清單中宣告 AppWidgetProvider 並實作。

在資訊清單中宣告小工具

首先,請在應用程式的 AndroidManifest.xml 檔案中宣告 AppWidgetProvider 類別,如以下範例所示:

<receiver android:name="ExampleAppWidgetProvider"
                 android:exported="false">
    <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。除非其他程序需要廣播至 AppWidgetProvider,否則請勿匯出這個元件,但通常並非如此。

<intent-filter> 元素必須包含帶有 android:name 屬性的 <action> 元素。這個屬性會指定 AppWidgetProvider 接受 ACTION_APPWIDGET_UPDATE 廣播。這是唯一必須明確宣告的廣播訊息。AppWidgetManager 會視需要自動將所有其他小工具廣播訊息傳送至 AppWidgetProvider

<meta-data> 元素會指定 AppWidgetProviderInfo 資源,並提供下列屬性:

  • android:name:指定中繼資料名稱。請使用 android.appwidget.provider 將資料識別為 AppWidgetProviderInfo 描述元。
  • android:resource:指定 AppWidgetProviderInfo 資源位置。

實作 AppWidgetProvider 類別

AppWidgetProvider 類別會將 BroadcastReceiver 擴充為方便處理小工具廣播訊息的便利類別。而是只會接收與小工具相關的廣播事件 (例如小工具更新、刪除、啟用和停用時)。這些廣播事件發生時,系統會呼叫以下 AppWidgetProvider 方法:

onUpdate()
系統會呼叫此方法,按照 AppWidgetProviderInfoupdatePeriodMillis 屬性定義的間隔更新小工具。詳情請參閱本頁的「其他小工具屬性」表格
當使用者新增小工具時,系統也會呼叫此方法,因此會執行基本設定,例如為 View 物件定義事件處理常式,或開始載入資料以在小工具中顯示的工作。不過,如果您在沒有 configuration_optional 旗標的情況下宣告設定活動,則當使用者新增小工具時,系統「不會」呼叫此方法,但後續更新「會」呼叫此方法。設定完成時,必須負責設定活動來執行第一次更新。詳情請參閱「允許使用者設定應用程式小工具」。
最重要的回呼是 onUpdate()。詳情請參閱本頁的「使用 onUpdate() 類別處理事件」一節。
onAppWidgetOptionsChanged()

在首次放置小工具以及每次調整小工具大小時呼叫此方法。使用這個回呼可根據小工具的大小範圍顯示或隱藏內容。呼叫 getAppWidgetOptions() 可傳回包含下列內容的 Bundle,以取得小工具執行個體可採用的大小範圍,以及自 Android 12 起的可能大小清單:

onDeleted(Context, int[])

每當從小工具主機中刪除小工具時,就會呼叫此方法。

onEnabled(Context)

首次建立小工具的執行個體時,系統會呼叫此方法。舉例來說,如果使用者新增兩個小工具例項,系統只會第一次呼叫這個元素。如果您需要開啟新的資料庫,或執行其他小工具執行個體只需執行一次的設定,則可以使用此方法。

onDisabled(Context)

當系統從小工具主機刪除小工具的最後例項時,就會呼叫此方法。您可以在這裡清除在 onEnabled(Context) 中執行的所有工作,例如刪除臨時資料庫。

onReceive(Context, Intent)

系統會為每個廣播呼叫,並在之前的回呼方法之前呼叫此方法。您通常不需要實作此方法,因為預設的 AppWidgetProvider 實作會視情況篩選所有小工具廣播,並視情況呼叫上述方法。

您必須使用 AndroidManifest 中的 <receiver> 元素,將 AppWidgetProvider 類別實作宣告為廣播接收器。詳情請參閱本頁的「在資訊清單中宣告小工具」。

透過 onUpdate() 類別處理事件

最重要的 AppWidgetProvider 回呼是 onUpdate(),因為每個小工具加入主機時都會呼叫這個方法,除非您使用沒有 configuration_optional 旗標的設定活動。如果您的小工具接受任何使用者互動事件,請在這個回呼中註冊事件處理常式。如果小工具不會建立暫存檔案或資料庫,或是執行其他需要清除的作業,則 onUpdate() 可能是唯一需要定義的回呼方法。

舉例來說,如果您希望小工具包含輕觸時啟動活動的按鈕,可以使用下列 AppWidgetProvider 實作:

Kotlin

class ExampleAppWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        appWidgetIds.forEach { appWidgetId ->
            // Create an Intent to launch ExampleActivity.
            val pendingIntent: PendingIntent = PendingIntent.getActivity(
                    /* context = */ context,
                    /* requestCode = */  0,
                    /* intent = */ Intent(context, ExampleActivity::class.java),
                    /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            val views: RemoteViews = RemoteViews(
                    context.packageName,
                    R.layout.appwidget_provider_layout
            ).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // Tell the AppWidgetManager to perform an update on the current
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

Java

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        for (int i=0; i < appWidgetIds.length; i++) {
            int appWidgetId = appWidgetIds[i];
            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(
                /* context = */ context,
                /* requestCode = */ 0,
                /* intent = */ intent,
                /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
            );

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

AppWidgetProvider 只會定義 onUpdate() 方法,用於建立可啟動 ActivityPendingIntent,並使用 setOnClickPendingIntent(int, PendingIntent) 將其附加至小工具按鈕。其中包含一個迴圈,可疊代 appWidgetIds 中的每個項目,這個 ID 陣列會識別此提供者建立的每個小工具。如果使用者建立多個小工具執行個體,則所有執行個體會同時更新。不過,所有小工具執行個體只能管理一個 updatePeriodMillis 時間表。舉例來說,如果將更新排程定義為每兩小時一次,而在第 1 小時之後新增第二個小工具執行個體,則這兩個執行個體都會在第一個更新週期定義的期間更新,而忽略第二個更新週期。兩者都每兩小時更新一次,而不是每小時更新一次。

詳情請參閱 ExampleAppWidgetProvider.java 範例類別。

接收小工具廣播意圖

AppWidgetProvider 是一種便利類別,如果想直接接收小工具廣播,可以實作自己的 BroadcastReceiver 或覆寫 onReceive(Context,Intent) 回呼。以下是您需要關注的意圖:

建立小工具版面配置

您必須在 XML 中為小工具定義初始版面配置,並將其儲存在專案的 res/layout/ 目錄中。詳情請參閱「設計指南」。

如果您熟悉版面配置,就能以更簡單的方式建立小工具版面配置。不過請注意,小工具版面配置以 RemoteViews 為基礎,這種版面配置不支援所有類型的版面配置或檢視畫面小工具。您無法使用 RemoteViews 支援的檢視畫面,也無法使用自訂檢視畫面或子類別。

RemoteViews 也支援 ViewStub,這是大小為零的隱藏 View,可用於在執行階段延後加載版面配置資源。

支援有狀態行為

Android 12 透過下列現有元件開始支援有狀態行為:

小工具仍為無狀態。您的應用程式必須儲存狀態並註冊狀態變更事件。

顯示有狀態行為的購物清單小工具範例
圖 3. 有狀態行為的範例。

以下程式碼範例說明如何實作這些元件。

Kotlin

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
        R.id.my_checkbox,
        RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)

Java

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true);

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2);

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
    R.id.my_checkbox,
    RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent));

提供兩個版面配置:一個指定在 res/layout-v31 中執行 Android 12 或更高版本的裝置,另一個則指定預設 res/layout 資料夾中的先前 Android 11 或以下版本。

實作圓角

Android 12 推出了下列系統參數,可用於設定小工具圓角的半徑:

以下範例顯示的小工具使用 system_app_widget_background_radius 做為小工具角落,並使用 system_app_widget_inner_radius 代表小工具中的檢視畫面。

小工具顯示小工具背景的半徑範圍,以及小工具內的檢視畫面
圖 4. 圓角。

1 小工具的邊角。

2 小工具內的檢視畫面角落。

圓角的重要注意事項

  • 第三方啟動器和裝置製造商可以將 system_app_widget_background_radius 參數覆寫至小於 28 dp。system_app_widget_inner_radius 參數一律小於 system_app_widget_background_radius 的值 8 dp。
  • 如果小工具未使用 @android:id/background,或定義根據外框剪輯內容的背景 (將 android:clipToOutline 設為 true),啟動器會自動識別背景,並使用圓角最多 16 dp 的矩形裁剪小工具。請參閱「確保小工具與 Android 12 相容」。

為了與舊版 Android 的小工具相容性,建議您定義自訂屬性,並使用自訂主題針對 Android 12 覆寫這些屬性,如以下 XML 檔案範例所示:

/values/attrs.xml

<resources>
  <attr name="backgroundRadius" format="dimension" />
</resources>

/values/styles.xml

<resources>
  <style name="MyWidgetTheme">
    <item name="backgroundRadius">@dimen/my_background_radius_dimen</item>
  </style>
</resources>

/values-31/styles.xml

<resources>
  <style name="MyWidgetTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
    <item name="backgroundRadius">@android:dimen/system_app_widget_background_radius</item>
  </style>
</resources>

/drawable/my_widget_background.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle">
  <corners android:radius="?attr/backgroundRadius" />
  ...
</shape>

/layout/my_widget_layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  android:background="@drawable/my_widget_background" />