创建简单的 widget

应用 widget 是小型应用视图,您可以将其嵌入到其他应用(例如主屏幕)中,并定期接收更新。这些视图在界面中称为 widget,您可以通过应用 widget 提供程序(或 widget 提供程序)发布一个视图。包含其他 widget 的应用组件称为应用 widget 宿主(或 widget 宿主)。图 1 显示了一个示例音乐微件:

音乐微件示例
图 1. 音乐 widget 示例。

本文档介绍了如何使用 widget 提供程序发布 widget。如需详细了解如何创建自己的 AppWidgetHost 来托管应用 widget,请参阅构建 widget 宿主

如需了解如何设计 widget,请参阅应用 widget 概览

微件组件

如需创建 widget,您需要以下基本组件:

AppWidgetProviderInfo 对象
描述 widget 的元数据,例如 widget 的布局、更新频率和 AppWidgetProvider 类。AppWidgetProviderInfo 在 XML 中定义,如本文档中所述。
AppWidgetProvider
定义可让您以程序化方式与 widget 交互的基本方法。通过它,您可以在更新、启用、停用或删除 widget 时收到广播。您可以在清单中声明 AppWidgetProvider,然后实现它(如本文档中所述)。
视图布局
定义 widget 的初始布局。布局是使用 XML 定义的,如本文档中所述。

图 2 显示了这些组件如何融入整个应用 widget 处理流程。

应用 widget 处理流程
图 2. 应用 widget 处理流程。

如果您的 widget 需要用户配置,请实现应用 widget 配置 activity。此 activity 允许用户修改 widget 设置,例如时钟 widget 的时区。

  • 从 Android 12(API 级别 31)开始,您可以提供默认配置,并允许用户稍后重新配置 widget。如需了解详情,请参阅使用 widget 的默认配置允许用户重新配置已放置的 widget
  • 在 Android 11(API 级别 30)或更低版本中,每当用户将 widget 添加到主屏幕时,系统都会启动此 activity。

此外,我们还建议您实现以下改进:灵活的微件布局其他增强功能高级微件集合微件以及构建微件托管应用

声明 AppWidgetProviderInfo XML

AppWidgetProviderInfo 对象定义了 widget 的基本特性。使用单个 <appwidget-provider> 元素在 XML 资源文件中定义 AppWidgetProviderInfo 对象,并将其保存在项目的 res/xml/ 文件夹中。

具体可见以下示例:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="120dp"
    android:updatePeriodMillis="86400000"
    android:description="@string/example_appwidget_description"
    android:previewLayout="@layout/example_appwidget_preview"
    android:initialLayout="@layout/example_loading_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>

微件大小调整属性

默认主屏幕根据已定义高度和宽度的单元格网格将 widget 放置在其窗口中。大多数主屏幕仅允许 widget 采用网格单元的整数倍数,例如,横向显示两个单元格,纵向三个单元格。

借助微件大小调整属性,您可以为微件指定默认大小,并提供微件大小的下限和上限。在此上下文中,widget 的默认大小是 widget 首次添加到主屏幕时采用的尺寸。

下表介绍了与微件大小调整相关的 <appwidget-provider> 属性:

属性和说明
targetCellWidthtargetCellHeight (Android 12)、minWidthminHeight
  • 从 Android 12 开始,targetCellWidthtargetCellHeight 属性以网格单元格的形式指定 widget 的默认大小。这些属性在 Android 11 及更低版本中会被忽略,如果主屏幕不支持基于网格的布局,则可以忽略这些属性。
  • minWidthminHeight 属性用于指定微件的默认大小(以 dp 为单位)。如果微件的最小宽度或高度的值与单元格的尺寸不匹配,则这些值将向上舍入到最接近的单元格大小。
我们建议您同时指定 targetCellWidthtargetCellHeight 以及 minWidthminHeight 这两组属性,以便在用户的设备不支持 targetCellWidthtargetCellHeight 时,您的应用可以回退到使用 minWidthminHeight。如果支持,则 targetCellWidthtargetCellHeight 属性优先于 minWidthminHeight 属性。
minResizeWidthminResizeHeight 指定 widget 的绝对最小尺寸。这些值会指定微件在多大尺寸下难以辨认或无法使用。使用这些属性,用户可以将微件的大小调整为小于默认微件大小。如果 minResizeWidth 属性大于 minWidth 或未启用水平大小调整,系统会忽略该属性。请参阅 resizeMode。同样,如果 minResizeHeight 属性大于 minHeight 或未启用垂直大小调整,则系统会忽略该属性。
maxResizeWidthmaxResizeHeight 指定微件的建议大小上限。如果这些值不是网格单元格尺寸的倍数,则这些值会四舍五入为最接近的单元格大小。如果 maxResizeWidth 属性小于 minWidth 或未启用水平大小调整,则该属性会被忽略。请参阅 resizeMode。同样,如果 maxResizeHeight 属性大于 minHeight 或未启用垂直大小调整,则系统会忽略该属性。 此元素在 Android 12 中引入。
resizeMode 指定可按什么规则来调整微件的大小。您可以使用此属性将主屏幕微件设为可在水平轴、垂直轴或两个轴上调整大小。用户轻触并按住微件以显示其大小调整手柄,然后拖动水平或垂直手柄以更改其在布局网格上的大小。resizeMode 属性的值包括 horizontalverticalnone。如需将微件声明为可在水平和垂直方向上调整大小,请使用 horizontal|vertical

示例

为了说明上表中的属性对微件大小调整有何影响,我们假设采用以下规范:

  • 网格单元格的宽度为 30dp,高度为 50dp。
  • 提供了以下属性规范:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="80dp"
    android:minHeight="80dp"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:minResizeWidth="40dp"
    android:minResizeHeight="40dp"
    android:maxResizeWidth="120dp"
    android:maxResizeHeight="120dp"
    android:resizeMode="horizontal|vertical" />

从 Android 12 开始

使用 targetCellWidthtargetCellHeight 属性作为 widget 的默认大小。

默认情况下,微件的大小为 2x2。该 widget 的尺寸可以缩小到 2x1 或最大 4x3。

Android 11 及更低版本

使用 minWidthminHeight 属性计算 widget 的默认大小。

默认宽度 = Math.ceil(80 / 30) = 3

默认高度 = Math.ceil(80 / 50) = 2

此微件的大小默认为 3x2。该 widget 的尺寸可以缩小到 2x1,或者最大为全屏。

其他微件属性

下表介绍了与除 widget 大小调整以外的质量相关的 <appwidget-provider> 属性。

属性和说明
updatePeriodMillis 定义微件框架通过调用 onUpdate() 回调方法从 AppWidgetProvider 请求更新的频率。使用该值不保证实际更新准时进行,我们建议您尽可能降低更新频率(每小时不超过一次),以节省电量。 如需查看选择适当更新期间的注意事项的完整列表,请参阅针对更新微件内容的优化
initialLayout 指向用于定义 widget 布局的布局资源。
configure 定义在用户添加微件时启动的 Activity,以便用户配置微件属性。请参阅允许用户配置 widget。 从 Android 12 开始,应用可以跳过初始配置。如需了解详情,请参阅使用微件的默认配置
description 指定要为微件显示的微件选择器的说明。此元素在 Android 12 中引入。
previewLayout (Android 12) 和 previewImage(Android 11 及更低版本)
  • 从 Android 12 开始,previewLayout 属性会指定可缩放的预览,您可以将其作为 XML 布局提供,该布局会设置为微件的默认尺寸。理想情况下,指定为该属性的布局 XML 与具有实际默认值的实际 widget 是相同的布局 XML。
  • 在 Android 11 或更低版本中,previewImage 属性指定 widget 配置后的预览,即用户在选择应用 widget 时会看到的预览。如果未提供,用户会看到应用的启动器图标。此字段对应于 AndroidManifest.xml 文件的 <receiver> 元素中的 android:previewImage 属性。
注意:我们建议您同时指定 previewImagepreviewLayout 属性,以便用户的设备不支持 previewLayout 时,您的应用可以回退到使用 previewImage。如需了解详情,请参阅与可扩缩的 widget 预览的向后兼容性
autoAdvanceViewId 指定由 widget 宿主自动前进的 widget 子视图的视图 ID。
widgetCategory 声明您的 widget 是否可以显示在主屏幕 (home_screen) 和/或锁定屏幕 (keyguard) 上。对于 Android 5.0 及更高版本,只有 home_screen 有效。
widgetFeatures 声明 widget 支持的功能。例如,如果您希望微件在用户添加它时使用其默认配置,请同时指定 configuration_optionalreconfigurable 标志。这样会绕过在用户添加 widget 后启动配置 activity 的步骤。之后,用户仍可重新配置该 widget

使用 AppWidgetProvider 类处理微件广播

AppWidgetProvider 类会处理 widget 广播并更新 widget,以响应 widget 生命周期事件。下面几部分介绍了如何在清单中声明 AppWidgetProvider 并实现它。

在清单中声明 widget

首先,在应用的 AndroidManifest.xml 文件中声明 AppWidgetProvider 类,如以下示例所示:

<receiver android:name="ExampleAppWidgetProvider"
                 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/example_appwidget_info" />
</receiver>

<receiver> 元素需要 android:name 属性,该属性指定 widget 使用的 AppWidgetProvider。不得导出该组件,除非有单独的进程需要广播到您的 AppWidgetProvider(通常情况并非如此)。

<intent-filter> 元素必须包含具有 android:name 属性的 <action> 元素。此属性指定 AppWidgetProvider 接受 ACTION_APPWIDGET_UPDATE 广播。这是您必须明确声明的唯一一项广播。AppWidgetManager 会根据需要自动将所有其他 widget 广播发送到 AppWidgetProvider

<meta-data> 元素可指定 AppWidgetProviderInfo 资源,并且需要以下属性:

  • android:name:指定元数据名称。使用 android.appwidget.provider 将数据标识为 AppWidgetProviderInfo 描述符。
  • android:resource:指定 AppWidgetProviderInfo 资源位置。

实现 AppWidgetProvider 类

AppWidgetProvider 类将 BroadcastReceiver 扩展为用于处理 widget 广播的便捷类。它仅接收与 widget 相关的事件广播,例如当 widget 的更新、删除、启用和停用时。当发生这些广播事件时,系统会调用以下 AppWidgetProvider 方法:

onUpdate()
调用此方法以按 AppWidgetProviderInfo 中的 updatePeriodMillis 属性定义的时间间隔更新 widget。如需了解详情,请参阅本页中描述其他微件属性的表
当用户添加 widget 时,系统也会调用此方法,因此它会执行基本设置,例如为 View 对象定义事件处理脚本,或启动作业以加载要在 widget 中显示的数据。不过,如果您在没有 configuration_optional 标志的情况下声明配置 activity,则用户添加 widget 时不会调用此方法,但会调用后续更新。配置 activity 负责在配置完成后执行首次更新。如需了解详情,请参阅允许用户配置应用微件
最重要的回调是 onUpdate()。如需了解详情,请参阅本页中的使用 onUpdate() 类处理事件
onAppWidgetOptionsChanged()

首次放置 widget 以及每次调整 widget 大小时,都会调用此方法。使用此回调可根据 widget 的大小范围显示或隐藏内容。通过调用 getAppWidgetOptions()(会返回包含以下内容的 Bundle),获取尺寸范围,并且从 Android 12 开始,还会列出 widget 实例可以采用的可能尺寸的列表:

onDeleted(Context, int[])

每次从 widget 宿主中删除 widget 时,都会调用此方法。

onEnabled(Context)

首次创建 widget 的实例时调用此方法。例如,如果用户添加了 widget 的两个实例,则此操作仅在第一次调用时调用。如果您需要打开新数据库或执行其他只需对所有 widget 实例执行一次的设置,则这是一个不错的选择。

onDisabled(Context)

从 widget 宿主中删除 widget 的最后一个实例时,会调用此方法。您可以在此处清理 onEnabled(Context) 中完成的所有工作,例如删除临时数据库。

onReceive(Context, Intent)

系统会为每个广播调用该方法,并且在上述每个回调方法之前调用该方法。您通常不需要实现此方法,因为默认的 AppWidgetProvider 实现会过滤所有 widget 广播,并视情况调用上述方法。

您必须使用 AndroidManifest 中的 <receiver> 元素将 AppWidgetProvider 类实现声明为广播接收器。如需了解详情,请参阅本页中的在清单中声明 widget

使用 onUpdate() 类处理事件

最重要的 AppWidgetProvider 回调是 onUpdate(),因为除非您使用不带 configuration_optional 标志的配置 activity,否则在将每个 widget 添加到宿主时都会调用它。如果微件接受任何用户互动事件,请在此回调中注册事件处理脚本。如果您的 widget 没有创建临时文件或数据库,也未执行其他需要清理的工作,那么 onUpdate() 可能是您需要定义的唯一回调方法。

例如,如果您希望某个 widget 具有一个在点按后启动 activity 的按钮,则可以使用 AppWidgetProvider 的以下实现:

Kotlin

class ExampleAppWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        appWidgetIds.forEach { appWidgetId ->
            // Create an Intent to launch ExampleActivity.
            val pendingIntent: PendingIntent = PendingIntent.getActivity(
                    /* context = */ context,
                    /* requestCode = */  0,
                    /* intent = */ Intent(context, ExampleActivity::class.java),
                    /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            val views: RemoteViews = RemoteViews(
                    context.packageName,
                    R.layout.appwidget_provider_layout
            ).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // Tell the AppWidgetManager to perform an update on the current
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

Java

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        for (int i=0; i < appWidgetIds.length; i++) {
            int appWidgetId = appWidgetIds[i];
            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(
                /* context = */ context,
                /* requestCode = */ 0,
                /* intent = */ intent,
                /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
            );

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

AppWidgetProvider 仅定义了 onUpdate() 方法,用于创建一个 PendingIntent,用于启动 Activity 并使用 setOnClickPendingIntent(int, PendingIntent) 将其附加到 widget 的按钮。它包含一个循环,用于遍历 appWidgetIds 中的每个条目;这是一个 ID 数组,用于标识由此提供程序创建的每个 widget。如果用户创建了 widget 的多个实例,则这些实例会同时更新。不过,微件的所有实例只能管理一个 updatePeriodMillis 时间表。例如,如果将更新时间表定义为每两小时一次,并在第一个实例实例一小时后添加另一个微件实例,则这两个实例都会在第一个更新周期定义的时间段进行更新,并忽略第二个更新周期。它们都是每两小时更新一次,而不是每小时更新一次。

如需了解详情,请参阅 ExampleAppWidgetProvider.java 示例类。

接收 widget 广播 intent

AppWidgetProvider 是一个辅助类。如果您想直接接收 widget 广播,可以实现自己的 BroadcastReceiver 或替换 onReceive(Context,Intent) 回调。您需要关注的 intent 如下:

创建 widget 布局

您必须在 XML 中定义 widget 的初始布局,并将其保存在项目的 res/layout/ 目录中。如需了解详情,请参阅设计准则

如果您熟悉布局,创建 widget 布局就非常简单。但请注意,widget 布局基于 RemoteViews,并不支持所有类型的布局或视图 widget。您无法使用自定义视图或 RemoteViews 支持的视图子类。

RemoteViews 还支持 ViewStub,这是一个不可见、零大小的 View,可用于在运行时延迟膨胀布局资源。

对有状态行为的支持

Android 12 使用以下现有组件添加了对有状态行为的支持:

微件仍然无状态。您的应用必须存储状态并注册状态更改事件。

显示有状态行为的购物清单 widget 示例
图 3. 有状态行为示例。

以下代码示例展示了如何实现这些组件。

Kotlin

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
        R.id.my_checkbox,
        RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)

Java

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true);

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2);

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
    R.id.my_checkbox,
    RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent));

res/layout-v31 中提供两种布局:一个布局面向搭载 Android 12 或更高版本的设备,另一种面向旧版 Android 11 或更低版本,位于默认的 res/layout 文件夹中。

实现圆角

Android 12 引入了以下系统参数来设置微件圆角的半径:

以下示例展示了一个微件,为微件的角使用 system_app_widget_background_radius,而为微件内的视图使用 system_app_widget_inner_radius

显示 widget 背景和 widget 内视图半径的 widget
图 4. 圆角。

1 微件的角。

2 微件内视图的角。

有关圆角的重要注意事项

  • 第三方启动器和设备制造商可以将 system_app_widget_background_radius 参数替换成小于 28 dp。system_app_widget_inner_radius 参数始终比 system_app_widget_background_radius 的值小 8 dp。
  • 如果您的 widget 未使用 @android:id/background 或定义根据轮廓裁剪其内容的背景(将 android:clipToOutline 设置为 true),启动器会自动识别背景并使用最大圆角为 16 dp 的矩形裁剪 widget。请参阅确保 widget 与 Android 12 兼容

为了使 widget 与之前版本的 Android 兼容,我们建议您为 Android 12 定义自定义属性并使用自定义主题替换这些属性,如以下 XML 示例文件所示:

/values/attrs.xml

<resources>
  <attr name="backgroundRadius" format="dimension" />
</resources>

/values/styles.xml

<resources>
  <style name="MyWidgetTheme">
    <item name="backgroundRadius">@dimen/my_background_radius_dimen</item>
  </style>
</resources>

/values-31/styles.xml

<resources>
  <style name="MyWidgetTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
    <item name="backgroundRadius">@android:dimen/system_app_widget_background_radius</item>
  </style>
</resources>

/drawable/my_widget_background.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle">
  <corners android:radius="?attr/backgroundRadius" />
  ...
</shape>

/layout/my_widget_layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  android:background="@drawable/my_widget_background" />