创建高级微件

本页介绍了创建更高级微件以提升用户体验的推荐做法。

更新微件内容的优化

更新微件内容可能需要大量的计算资源。为了节省电量,请优化更新类型、频率和时间。

微件更新的类型

更新微件有三种方式:完全更新、部分更新,以及对于集合微件而言,数据刷新。每种方式都有不同的计算成本和影响。

下文介绍了每种更新类型,并提供了相应的代码片段。

  • 完全更新:调用 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() 方法,您可以在其中放置更新微件的代码。但是,如果您的微件需要异步加载数据或更新时间超过 10 秒,请考虑下文描述的广播接收器更新的替代方案,因为 10 秒后,系统会认为 BroadcastReceiver 无响应。

updatePeriodMillis 不支持小于 30 分钟的值。但是,如果您想禁用周期性更新,可以指定 0。

您可以在配置中允许用户调整更新频率。例如,他们可能希望股票行情每 15 分钟更新一次,或者每天仅更新四次。在这种情况下,请将 updatePeriodMillis 设置为 0,并改用 WorkManager

响应用户交互更新

以下是根据用户交互更新微件的一些推荐方法

  • 从应用的 activity 调用:响应用户交互(例如用户的轻触)时,直接调用 AppWidgetManager.updateAppWidget

  • 从远程交互(例如通知或应用微件)调用:构建 PendingIntent,然后从调用的 ActivityBroadcastService 更新微件。您可以选择自己的优先级。例如,如果为 PendingIntent 选择 Broadcast,您可以选择前台广播,以赋予 BroadcastReceiver 优先级。

响应广播事件更新

需要微件更新的广播事件示例是用户拍照。在这种情况下,您需要在检测到新照片时更新微件。

您可以使用 JobScheduler 调度作业,并使用 JobInfo.Builder.addTriggerContentUri 方法指定广播作为触发器。

您也可以注册广播的 BroadcastReceiver,例如监听 ACTION_LOCALE_CHANGED。但是,由于这会消耗设备资源,请谨慎使用并仅监听特定广播。随着 Android 7.0 (API level 24) 和 Android 8.0 (API level 26) 中引入广播限制,应用无法在其清单中注册隐式广播,但存在一些例外情况

从 BroadcastReceiver 更新微件时的注意事项

如果微件从 BroadcastReceiver(包括 AppWidgetProvider)更新,请注意以下关于微件更新持续时间和优先级的注意事项。

更新的持续时间

一般来说,系统允许广播接收器(通常在应用的主线程中运行)最多运行 10 秒,然后才会认为它们无响应并触发应用无响应 (ANR) 错误。为避免在处理广播时阻塞主线程,请使用 goAsync 方法。如果更新微件需要更长时间,请考虑使用 WorkManager 调度任务。

Caution: Any work you do here blocks further broadcasts until it completes,
so it can slow the receiving of later events.

如需了解详情,请参阅安全注意事项和最佳实践

更新的优先级

默认情况下,广播(包括使用 AppWidgetProvider.onUpdate 发出的广播)作为后台进程运行。这意味着系统资源过载可能会导致广播接收器的调用延迟。要优先处理广播,请将其设为前台进程。

例如,当用户轻触微件的某个部分时,将 Intent.FLAG_RECEIVER_FOREGROUND 标志添加到传递给 PendingIntent.getBroadcastIntent 中。

构建包含动态项目的准确预览

图 1:没有显示列表项的微件预览。

本部分介绍了为具有集合视图的微件(即使用 ListViewGridViewStackView 的微件)显示包含多个项目的微件预览的推荐方法。

如果您的微件使用这些视图之一,通过直接提供实际微件布局来创建可伸缩预览,当微件预览不显示任何项目时,用户体验会下降。这是因为集合视图数据是在运行时动态设置的,并且它看起来类似于图 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>

要提供模拟列表项,您可以多次包含布局,但这会导致每个列表项都相同。要提供唯一的列表项,请按照以下步骤操作

  1. 为文本值创建一组属性

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. 使用这些属性设置文本

    <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>
    
  3. 根据预览需要创建任意数量的样式。在每种样式中重新定义值

    <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>
    
  4. 在预览布局中的模拟项目上应用样式

    <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>