本页介绍了创建更高级的 widget 以提供更优质的用户体验的推荐做法。
用于更新 widget 内容的优化
更新 widget 内容的计算开销可能很大。为了节省电池消耗,请优化更新类型、频率和时间。
微件更新的类型
您可以通过以下三种方式更新 widget:完全更新、部分更新,以及(对于集合 widget)数据刷新。每种方法都有不同的计算成本和影响。
下文介绍了每种更新类型,并提供了每种类型的代码段。
完全更新:调用
AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews)
以完全更新 widget。这会将之前提供的RemoteViews
替换为新的RemoteViews
。这是计算成本最高的更新。Kotlin
val appWidgetManager = AppWidgetManager.getInstance(context) val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also { setTextViewText(R.id.textview_widget_layout1, "Updated text1") setTextViewText(R.id.textview_widget_layout2, "Updated text2") } appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
Java
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout); remoteViews.setTextViewText(R.id.textview_widget_layout1, "Updated text1"); remoteViews.setTextViewText(R.id.textview_widget_layout2, "Updated text2"); appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
部分更新:调用
AppWidgetManager.partiallyUpdateAppWidget
以更新 widget 的部分内容。此操作会将新的RemoteViews
与之前提供的RemoteViews
合并。如果 widget 未通过updateAppWidget(int[], RemoteViews)
至少接收一次完整更新,则此方法会被忽略。Kotlin
val appWidgetManager = AppWidgetManager.getInstance(context) val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also { setTextViewText(R.id.textview_widget_layout, "Updated text") } appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)
Java
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout); remoteViews.setTextViewText(R.id.textview_widget_layout, "Updated text"); appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews);
集合数据刷新:调用
AppWidgetManager.notifyAppWidgetViewDataChanged
使 widget 中集合视图的数据失效。这会触发RemoteViewsFactory.onDataSetChanged
。在此期间,widget 中会显示旧数据。您可以使用此方法安全地同步执行耗时任务。Kotlin
val appWidgetManager = AppWidgetManager.getInstance(context) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
Java
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview);
只要应用具有与相应 AppWidgetProvider
类相同的 UID,您就可以从应用中的任何位置调用这些方法。
确定更新 widget 的频率
系统会根据为 updatePeriodMillis
属性提供的值定期更新 widget。微件可以响应用户互动、广播更新,也可以同时响应用户互动和广播更新。
定期更新
您可以在 appwidget-provider
XML 中为 AppWidgetProviderInfo.updatePeriodMillis
指定一个值,以控制定期更新的频率。每次更新都会触发 AppWidgetProvider.onUpdate()
方法,您可以在其中放置用于更新 widget 的代码。不过,如果您的 widget 需要异步加载数据或更新时间超过 10 秒,请考虑下一部分中介绍的广播接收器更新替代方案,因为 10 秒后,系统会将 BroadcastReceiver
视为无响应。
updatePeriodMillis
不支持小于 30 分钟的值。不过,如果您想停用定期更新,可以指定 0。
您可以让用户在配置中调整更新频率。例如,他们可能希望股票行情自动收录器每 15 分钟更新一次,或者一天只更新 4 次。在这种情况下,请将 updatePeriodMillis
设置为 0,并改用 WorkManager
。
根据用户互动进行更新
以下是一些建议的方法,可用于根据用户互动更新 widget:
从应用的 activity 中:直接调用
AppWidgetManager.updateAppWidget
以响应用户互动(例如用户点按)。通过远程互动(例如通知或应用 widget):构建
PendingIntent
,然后从调用的Activity
、Broadcast
或Service
更新 widget。您可以自行选择优先级。例如,如果您为PendingIntent
选择Broadcast
,则可以选择前台广播来为BroadcastReceiver
赋予优先级。
根据广播事件进行更新
需要 widget 更新的广播事件的一个示例是用户拍照时。在这种情况下,您希望在检测到新照片时更新 widget。
您可以使用 JobScheduler
调度作业,并使用 JobInfo.Builder.addTriggerContentUri
方法指定广播作为触发器。
您还可以为广播注册 BroadcastReceiver
,例如监听 ACTION_LOCALE_CHANGED
。不过,由于这会消耗设备资源,因此请谨慎使用此功能,并且仅监听特定的广播。随着 Android 7.0(API 级别 24)和 Android 8.0(API 级别 26)中引入了广播限制,应用无法在其清单中注册隐式广播,但存在某些例外情况。
从 BroadcastReceiver 更新 widget 时的注意事项
如果 widget 是从 BroadcastReceiver
(包括 AppWidgetProvider
)更新的,请注意以下有关 widget 更新时长和优先级的注意事项。
更新时长
一般来说,系统会允许广播接收器(通常在应用的主线程中运行)运行长达 10 秒,然后才会认为它们无响应并触发应用无响应 (ANR) 错误。为避免在处理广播时阻塞主线程,请使用 goAsync
方法。如果更新 widget 需要更长时间,请考虑使用 WorkManager
安排任务。
Caution: Any work you do here blocks further broadcasts until it completes,
so it can slow the receiving of later events.
如需了解详情,请参阅安全注意事项和最佳实践。
更新的优先级
默认情况下,广播(包括使用 AppWidgetProvider.onUpdate
进行的广播)作为后台进程运行。这意味着,过载的系统资源可能会导致广播接收器的调用延迟。如需优先处理广播,请将其设为前台进程。
例如,当用户点按 widget 的某个部分时,向传递给 PendingIntent.getBroadcast
的 Intent
添加 Intent.FLAG_RECEIVER_FOREGROUND
标志。
构建包含动态商品的准确预览

本部分介绍了针对具有集合视图的 widget(即使用 ListView
、GridView
或 StackView
的 widget)在 widget 预览中显示多个项的推荐方法。
如果您的 widget 使用了上述某种视图,那么通过直接提供实际 widget 布局来创建可缩放的预览会在 widget 预览不显示任何项时降低用户体验。这是因为集合视图数据是在运行时动态设置的,并且看起来与图 1 中显示的图片类似。
为了使包含集合视图的 widget 预览在 widget 选择器中正确显示,我们建议维护一个专门用于预览的单独布局文件。此单独的布局文件包含实际的 widget 布局和一个包含虚假项的占位集合视图。例如,您可以通过提供包含多个虚假列表项的占位符 LinearLayout
来模拟 ListView
。
为了说明 ListView
的示例,我们先从一个单独的布局文件开始:
// res/layout/widget_preview.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/widget_background"
android:orientation="vertical">
// Include the actual widget layout that contains ListView.
<include
layout="@layout/widget_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// The number of fake items you include depends on the values you provide
// for minHeight or targetCellHeight in the AppWidgetProviderInfo
// definition.
<TextView android:text="@string/fake_item1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="?attr/appWidgetInternalPadding" />
<TextView android:text="@string/fake_item2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="?attr/appWidgetInternalPadding" />
</LinearLayout>
在提供 AppWidgetProviderInfo
元数据的 previewLayout
属性时,指定预览布局文件。您仍然需要为 initialLayout
属性指定实际 widget 布局,并在运行时构建 RemoteViews
时使用实际 widget 布局。
<appwidget-provider
previewLayout="@layout/widget_previe"
initialLayout="@layout/widget_view" />
复杂列表项
上一部分中的示例提供了虚假列表项,因为列表项是 TextView
对象。如果项是复杂的布局,则提供虚假项可能会更复杂。
假设在 widget_list_item.xml
中定义了一个列表项,该列表项包含两个 TextView
对象:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:id="@id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fake_title" />
<TextView android:id="@id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fake_content" />
</LinearLayout>
如需提供虚假列表项,您可以多次包含该布局,但这会导致每个列表项都相同。如需提供唯一的列表项,请按以下步骤操作:
为文本值创建一组属性:
<resources> <attr name="widgetTitle" format="string" /> <attr name="widgetContent" format="string" /> </resources>
使用以下属性设置文本:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="?widgetTitle" /> <TextView android:id="@id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="?widgetContent" /> </LinearLayout>
根据预览需要创建任意数量的样式。重新定义每种样式中的值:
<resources> <style name="Theme.Widget.ListItem"> <item name="widgetTitle"></item> <item name="widgetContent"></item> </style> <style name="Theme.Widget.ListItem.Preview1"> <item name="widgetTitle">Fake Title 1</item> <item name="widgetContent">Fake content 1</item> </style> <style name="Theme.Widget.ListItem.Preview2"> <item name="widgetTitle">Fake title 2</item> <item name="widgetContent">Fake content 2</item> </style> </resources>
在预览布局中对虚假项应用样式:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" ...> <include layout="@layout/widget_view" ... /> <include layout="@layout/widget_list_item" android:theme="@style/Theme.Widget.ListItem.Preview1" /> <include layout="@layout/widget_list_item" android:theme="@style/Theme.Widget.ListItem.Preview2" /> </LinearLayout>