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.