Android App Widget 是 app 的一個擴充功能。使用者可以在 home screen 上擺放 app 提供的 app widgets。App 可以透過 app widgets 提供一個資訊或簡單的功能,而不需要使用者執行 app。例如,時鐘 app 提供一個顯示當前時間的 app widget。那使用者在 home screen 就 app widget 就可以看到時間,而不需要開啟時鐘 app 來看時間。本文章將介紹如何開發一個簡單的 app widget。
Table of Contents
建立一個 App Widget
在專案中建立一個 app widget 是很容易的事。如下圖,在專案中,選擇 New -> Widget -> App Widget。

接下來,輸入 app widget 的 class 名稱。在 Resizable 欄位中,我們可以選擇可 resize 的方向。在 Minimum Widget 和 Minimum Height 欄位中,輸入 app widget 最小的大小。

然後,Android Studio 會幫我們產生出所有需要的程式碼。當然,你也可以手動自己一個一個地新增這些程式碼。現在我們已經有一個可用的 app widget。你可以在 emulator 中安裝此 app,並且在 home screen 上,顯示 PersonInfoAppWidget。

接下來,我們要修改 PersonInfoAppWidget 成可以顯示個人資訊的 app widget。
將 person_info_app_widget.xml 修改成如下。從此 layout 可看出,PersonInfoAppWidget 會顯示個人的名字與工作。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.AppWidgetExample.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/Theme.AppWidgetExample.AppWidgetContainer">
<LinearLayout
style="@style/Widget.AppWidgetExample.AppWidget.InnerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textStyle="bold|italic" />
<TextView
android:id="@+id/job_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd"
android:textStyle="bold|italic" />
</LinearLayout>
</RelativeLayout>將 PersonInfoAppWidget.kt 修改成如下。再執行一次此 app,我們可以看到 PersonInfoAppWidget 會顯示個人的名字與工作。
package com.waynestalk.appwidget
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
val wayne = Person(
name = "Wayne",
job = "Software programmer",
website = "https://waynestalk.com/",
github = "https://github.com/xhhuango",
)
/**
* Implementation of App Widget functionality.
*/
class PersonInfoAppWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
) {
// Construct the RemoteViews object
val view = RemoteViews(context.packageName, R.layout.person_info_app_widget)
view.setTextViewText(R.id.name_text_view, wayne.name)
view.setTextViewText(R.id.job_text_view, wayne.job)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, view)
}AppWidgetProvider
PersonInfoAppWidget 繼承 AppWidgetProvider,而 AppWidgetProvider 繼承 BroadcastReceiver。所以當 app widget 有任何更動時,AppWidgetProvider 都會接收到 broadcasts。
以下是常用的 AppWidgetProvider 的 methods:
- onUpdate:當 app widget 被要求提供 RemoteViews 時。我們至少要實作此 method 來提供 app widget 的畫面。
- onDeleted:當一個或多個 app widget 被刪除時,如從 home screen 上被刪除。
- onEnabled:當一個 app widget 被新增時。
- onDisabled:當最後一個 app widget 被刪除時。
- onAppWidgetOptionsChanged:當一個 app widget 被 resize,或是它的 options 被改變時。
每一個 app widget 可以在 home screen 上有多的 instances。因此,每一個 instance 都有一個 appWidgetId。所以,AppWidgetProvider 的 methods 大多有 appWidgetId 參數,這樣 AppWidgetProvider 就可以知道哪一個 instance 被更動。
因為 AppWidgetProvider 是 BroadcastReceiver,所以我們必須要將它宣告在 AndroidManifest.xml 裡。如果你是用 Android Studio 來新增 app widget 的話,那 Android Studio 會自動幫我們在 AndriodManifest.xml 中宣告好。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppWidgetExample"
tools:targetApi="31">
<receiver
android:name=".PersonInfoAppWidget"
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/person_info_app_widget_info" />
</receiver>
</application>
</manifest>RemoteViews
AppWidget 只能提供 RemoteViews。RemoteViews 的內容可以從一個 layout 來 inflated。不過,RemoteViews 只有支援某些 layouts。這裡列出它支援的所有 layouts。我們可以看出,RemoteViews 只有支援 android 的 layouts,而不支援 androidx 的 layouts。
當我們要變更 layout 中的 views 時,我們不能直接存取某個 view,而是必須要透過 RemoteViews 的方法。如在 PersonInfoAppWidget 中,我們要將名字設定給 TextView 時,我們不能直接存取該 TextView,而必須要透過 RemoteViews.setTextViewText()。
val view = RemoteViews(context.packageName, R.layout.person_info_app_widget) view.setTextViewText(R.id.name_text_view, wayne.name) view.setTextViewText(R.id.job_text_view, wayne.job)
AppWidgetProviderInfo XML
在 AndroidManifest.xml 中宣告 AppWidgetProvider 時,我們要在 <receiver/> 裡用 <meta-data/> 宣告 AppWidgetProviderInfo。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application >
<receiver
android:name=".PersonInfoAppWidget"
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/person_info_app_widget_info" />
</receiver>
</application>
</manifest>如果你是用 Android Studio 來新增 app widget 時,它會自動幫我們產生一個 AppWidgetProviderInfo。在此專案中,Android Studio 產生的是 person_info_widget_info.xml。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/person_info_app_widget"
android:initialLayout="@layout/person_info_app_widget"
android:minWidth="110dp"
android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview"
android:previewLayout="@layout/person_info_app_widget"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="2"
android:targetCellHeight="1"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />以下我們大概說明一下 <appwidget-provider/> 的各個屬性,詳情請參考官網。
| Attributes | Description |
|---|---|
| targetCellWidth targetCellHeight minWidth minHeight | Android 12 開始,targetCellWidth 和 targetCellHeight 指定 app widget 的預設大小。如果 home screen 不支援 grid-based layout 的話,就會改用 minWidth 和 minHeight。 Android 11 與 lower,minWidth 和 minHeight 指定 app widget 的預設大小。當 minWidth 和 minHeight 無法對應 cells 的大小,其值會被 rounded up 到最近的 cell 大小。 |
| minResizeWidth minResizeHeight | 指定 app widget 絕對的最小大小。 |
| maxResizeWidth maxResizeHeight | 指定 app widget 推薦的最大大小。 |
| resizeMode | 指定 app widget 可 resize 的方向。horizontal、vertical、none、horizontal|vertical。 |
| initialLayout | app widget 的 layout resource。 |
| description | 在 widget picker 中顯示此 app widget 的說明。 |
| previewImage previewLayout | Android 12 開始,previewLayout 指定 scalable 預覽。 Android 11 與 lower,previewImage 指定預覽圖。 Widget picker 會顯示此預覽。 |
| updatePeriodMillis | 指定多久 widget framework 會呼叫 AppWidgetProvider.onUpdate() 來更新 app widget 的 view。 |
| widgetCategory | 指定此 app widget 可顯示在 home scree(home_screen)或 lock screen(keyguard)。 |
提供 Responsive Layout
當 app widget 被改變大小時,我們可以根據不同的大小,提供不同的 layout。
當 PersonInfoAppWidget 被拉大時,我們將提供以下的 layout。以下的 layout 除了顯示個人的名字與工作之外,還會顯示 Website 和 GitHub。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.AppWidgetExample.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/Theme.AppWidgetExample.AppWidgetContainer">
<LinearLayout
style="@style/Widget.AppWidgetExample.AppWidget.InnerView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold|italic" />
<TextView
android:id="@+id/job_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd"
android:textStyle="bold|italic" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Website:"
android:textStyle="bold|italic" />
<TextView
android:id="@+id/website_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd"
android:textStyle="bold|italic" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GitHub:"
android:textStyle="bold|italic" />
<TextView
android:id="@+id/github_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd"
android:textStyle="bold|italic" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>我們可以用將 view 與其相對應的大小放到一個 Map 裡,再將 map 設定給 AppWidgetManager.updateAppWidget()。當 PersonInfoAppWidget 被顯示時,會依據它的大小,選擇最接近的 layout 來顯示。
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
) {
// Construct the RemoteViews object
val simpleView = RemoteViews(context.packageName, R.layout.person_info_app_widget)
simpleView.setTextViewText(R.id.name_text_view, wayne.name)
simpleView.setTextViewText(R.id.job_text_view, wayne.job)
val detailView = RemoteViews(context.packageName, R.layout.person_info_detail_app_widget)
detailView.setTextViewText(R.id.name_text_view, wayne.name)
detailView.setTextViewText(R.id.job_text_view, wayne.job)
detailView.setTextViewText(R.id.website_text_view, wayne.website)
detailView.setTextViewText(R.id.github_text_view, wayne.github)
val viewMapping = mapOf(
SizeF(60f, 100f) to simpleView,
SizeF(100f, 200f) to detailView,
)
val views = RemoteViews(viewMapping)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}結語
在專案中建立一個 app widget 時,需要不少的步驟。但是,如果使用 Android Studio 來建立一個 app widget 的話,那是非常輕鬆的事了。它會自動幫我們產生所有必要的程式碼。我們可以基於這些程式碼再去修改,這讓我們省去很多時間。
參考
- Create a simple widget, Google developers.









