本页介绍了创建更高级别小部件以获得更好的用户体验的最佳实践。
更新小部件内容的优化
更新小部件内容可能会占用大量计算资源。为了节省电池消耗,请优化更新类型、频率和时间。
小部件更新的类型
有三种方法可以更新小部件:完整更新、部分更新,以及在集合小部件的情况下,数据刷新。每种方法都有不同的计算成本和影响。
以下描述了每种更新类型并提供了每种类型的代码片段。
完整更新:调用
AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews)
以完全更新小部件。这会用新的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
以更新小部件的某些部分。这会将新的RemoteViews
与之前提供的RemoteViews
合并。如果小部件没有通过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
以使小部件中集合视图的数据失效。这将触发RemoteViewsFactory.onDataSetChanged
。在此期间,将显示小部件中的旧数据。您可以安全地使用此方法同步执行昂贵的任务。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,您就可以在应用程序的任何位置调用这些方法。
确定更新小部件的频率
小部件会根据为 updatePeriodMillis
属性提供的价值定期更新。小部件可以响应用户交互、广播更新或两者的组合进行更新。
定期更新
您可以通过在 appwidget-provider
XML 中为 AppWidgetProviderInfo.updatePeriodMillis
指定一个值来控制定期更新的频率。每次更新都会触发 AppWidgetProvider.onUpdate()
方法,您可以在其中放置更新小部件的代码。但是,请考虑以下部分中介绍的 广播接收器更新的替代方法,如果您的 Widget 需要异步加载数据或更新时间超过 10 秒,因为在 10 秒后,系统会将 BroadcastReceiver
视为无响应。
updatePeriodMillis
不支持小于 30 分钟的值。但是,如果您想禁用定期更新,可以指定 0。
您可以让用户在配置中调整更新的频率。例如,他们可能希望股票行情每 15 分钟更新一次,或者每天只更新四次。在这种情况下,请将 updatePeriodMillis
设置为 0,并改用 WorkManager
。
响应用户交互进行更新
以下是一些根据用户交互更新小部件的推荐方法
从应用程序的活动中:直接调用
AppWidgetManager.updateAppWidget
以响应用户交互,例如用户的点击。从远程交互中(例如通知或应用小部件):构建一个
PendingIntent
,然后从调用的Activity
、Broadcast
或Service
更新小部件。您可以选择自己的优先级。例如,如果您为PendingIntent
选择Broadcast
,则可以选择 前台广播 以使BroadcastReceiver
优先级更高。
响应广播事件进行更新
需要小部件更新的广播事件的一个示例是用户拍摄照片。在这种情况下,您希望在检测到新照片时更新小部件。
您可以使用 JobScheduler
安排一项任务,并使用 JobInfo.Builder.addTriggerContentUri
方法指定广播作为触发器。
您还可以注册一个 BroadcastReceiver
来接收广播,例如,监听 ACTION_LOCALE_CHANGED
。但是,由于这会消耗设备资源,请谨慎使用,并且只监听特定广播。随着 Android 7.0 (API 级别 24) 和 Android 8.0 (API 级别 26) 中 广播限制 的引入,应用无法在其清单中注册隐式广播,某些 例外情况 除外。
从 BroadcastReceiver 更新小部件时的注意事项
如果小部件是从 BroadcastReceiver
更新的,包括 AppWidgetProvider
,请注意以下关于小部件更新持续时间和优先级的注意事项。
更新的持续时间
一般情况下,系统会让广播接收器(通常在应用程序的主线程中运行)运行最多 10 秒,然后再将其视为无响应并触发 应用程序无响应 (ANR) 错误。如果更新小部件需要更长时间,请考虑以下替代方法
使用
WorkManager
安排任务。使用
goAsync
方法为接收器提供更多时间。这将使接收器运行 30 秒。
有关更多信息,请参阅 安全注意事项和最佳实践。
更新的优先级
默认情况下,广播(包括使用 AppWidgetProvider.onUpdate
执行的广播)作为后台进程运行。这意味着系统资源过载会导致广播接收器调用延迟。要使广播优先级更高,请将其设置为前台进程。
例如,在用户点击小部件的某个部分时,将 Intent.FLAG_RECEIVER_FOREGROUND
标志添加到传递给 PendingIntent.getBroadcast
的 Intent
中。
构建包含动态项目的准确预览
本节介绍了为具有 集合视图 的小部件(即使用 ListView
、GridView
或 StackView
的小部件)显示小部件预览中的多个项目时的推荐方法。
如果您的 Widget 使用了其中一个视图,则通过 直接提供实际的小部件布局 来创建可扩展的预览,当小部件预览没有显示任何项目时,会降低用户体验。这是因为集合视图数据是在运行时动态设置的,它看起来类似于图 1 中所示的图像。
为了使具有集合视图的小部件的预览能够在小部件选择器中正确显示,我们建议维护一个单独的布局文件,专门用于预览。此单独的布局文件包含实际的小部件布局和一个带有假项目的占位符集合视图。例如,您可以通过提供一个包含几个假列表项的占位符 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
属性指定实际的小部件布局,并在运行时构造 RemoteViews
时使用实际的小部件布局。
<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>