创建简单小部件

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

Example of music widget
图 1. 音乐小部件示例。

本文档介绍了如何使用小部件提供程序发布小部件。有关创建您自己的AppWidgetHost 来托管应用小部件的详细信息,请参阅构建小部件主机.

有关如何设计小部件的信息,请参阅应用小部件概述.

小部件组件

要创建小部件,您需要以下基本组件

AppWidgetProviderInfo 对象
描述小部件的元数据,例如小部件的布局、更新频率和 AppWidgetProvider 类。AppWidgetProviderInfoXML 中定义,如本文档所述。
AppWidgetProvider
定义了允许您以编程方式与小部件交互的基本方法。通过它,您会在小部件更新、启用、禁用或删除时收到广播。您在清单中声明 AppWidgetProvider,然后实现 它,如本文档所述。
视图布局
定义小部件的初始布局。布局在XML 中定义,如本文档所述。

图 2 显示了这些组件如何在整个应用小部件处理流程中相互配合。

App widget processing flow
图 2. 应用小部件处理流程。

如果您的一个小部件需要用户配置,请实现应用小部件配置活动。此活动允许用户修改小部件设置,例如时钟小部件的时区。

我们还建议以下改进:灵活的小部件布局其他增强功能高级小部件集合小部件构建小部件主机.

声明 AppWidgetProviderInfo XML

The AppWidgetProviderInfo 对象定义了小部件的基本属性。使用单个 <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>

小部件大小属性

默认的主屏幕根据具有定义高度和宽度的单元格网格将小部件放置在其窗口中。大多数主屏幕只允许小部件采用网格单元格的整数倍的大小,例如,水平两个单元格乘以垂直三个单元格。

小部件大小属性允许您指定小部件的默认大小,并提供小部件大小的下限和上限。在这种情况下,小部件的默认大小是小部件第一次添加到主屏幕时所采用的大小。

下表描述了与小部件大小相关的 <appwidget-provider> 属性

属性和描述
targetCellWidthtargetCellHeight(Android 12)、minWidthminHeight
  • 从 Android 12 开始,targetCellWidthtargetCellHeight 属性以网格单元格为单位指定小部件的默认大小。这些属性在 Android 11 及更低版本中会被忽略,并且如果主屏幕不支持基于网格的布局,则可以被忽略。
  • The minWidth and minHeight attributes specify the default size of the widget in dp. If the values for a widget's minimum width or height don't match the dimensions of the cells, then the values are rounded up to the nearest cell size.
我们建议指定两组属性(targetCellWidthtargetCellHeight,以及 minWidthminHeight),以便如果用户的设备不支持 targetCellWidthtargetCellHeight,您的应用可以回退使用 minWidthminHeight。如果支持,targetCellWidthtargetCellHeight 属性优先于 minWidthminHeight 属性。
minResizeWidthminResizeHeight 指定小部件的绝对最小大小。这些值指定小部件变得难以辨认或无法使用的大小。使用这些属性,用户可以将小部件调整为小于默认小部件大小的大小。如果 minResizeWidth 属性大于 minWidth 或水平调整大小未启用,则该属性会被忽略。请参阅resizeMode。同样,如果 minResizeHeight 属性大于 minHeight 或垂直调整大小未启用,则该属性会被忽略。
maxResizeWidthmaxResizeHeight 指定小部件的推荐最大大小。如果值不是网格单元格尺寸的倍数,则它们将向上舍入到最接近的单元格大小。如果 maxResizeWidth 属性小于 minWidth 或水平调整大小未启用,则该属性会被忽略。请参阅resizeMode。同样,如果 maxResizeHeight 属性大于 minHeight 或垂直调整大小未启用,则该属性会被忽略。在 Android 12 中引入。
resizeMode 指定小部件可以调整大小的规则。您可以使用此属性使主屏幕小部件水平、垂直或在两个轴上都可以调整大小。用户触摸并按住小部件以显示其调整大小手柄,然后拖动水平或垂直手柄以更改其在布局网格上的大小。resizeMode 属性的值包括 horizontalverticalnone。要将小部件声明为水平和垂直可调整大小,请使用 horizontal|vertical

示例

为了说明前表中属性如何影响小部件的大小,假设以下规格

  • 一个网格单元格宽 30 dp,高 50 dp。
  • 提供以下属性规范
<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 属性作为小部件的默认大小。

小部件的默认大小为 2x2。小部件可以调整大小到 2x1 或 4x3。

Android 11 及更低版本

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

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

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

小部件的默认大小为 3x2。小部件可以调整大小到 2x1 或全屏。

其他小部件属性

下表描述了与小部件大小无关的其他质量相关的<appwidget-provider> 属性。

属性和描述
updatePeriodMillis 定义小部件框架多久调用一次onUpdate() 回调方法,以向AppWidgetProvider 请求更新。实际更新并不保证完全按照此值的时间进行,建议尽可能减少更新频率,每小时不超过一次,以节省电量。有关选择适当更新周期的完整注意事项列表,请参阅 更新小部件内容的优化
initialLayout 指向定义小部件布局的布局资源。
configure 定义当用户添加小部件时启动的活动,允许他们配置小部件属性。请参阅 启用用户配置小部件。从 Android 12 开始,您的应用可以跳过初始配置。有关详细信息,请参阅 使用小部件的默认配置
description 为小部件选择器指定描述,以显示您的 widget。在 Android 12 中引入。
previewLayout (Android 12) 和 previewImage (Android 11 及更低版本)
  • 从 Android 12 开始,previewLayout 属性指定一个可缩放的预览,您将其提供为设置为小部件默认大小的 XML 布局。理想情况下,作为此属性指定的布局 XML 与具有现实默认值的小部件实际布局 XML 相同。
  • 在 Android 11 或更低版本中,previewImage 属性指定小部件配置后的外观预览,用户在选择小部件时会看到此预览。如果未提供,用户将看到您的应用的启动器图标。此字段对应于 AndroidManifest.xml 文件中 <receiver> 元素中的 android:previewImage 属性。
注意:建议同时指定 previewImagepreviewLayout 属性,以便您的应用可以在用户的设备不支持 previewLayout 时回退到使用 previewImage。有关更多详细信息,请参阅 与可缩放小部件预览向后兼容
autoAdvanceViewId 指定由小部件主机自动前进的小部件子视图的视图 ID。
widgetCategory 声明您的 widget 是否可以在主屏幕 (home_screen)、锁屏 (keyguard) 或两者上显示。对于 Android 5.0 及更高版本,仅 home_screen 有效。
widgetFeatures 声明小部件支持的功能。例如,如果您希望您的 widget 在用户添加时使用其默认配置,请同时指定 configuration_optionalreconfigurable 标志。这将绕过在用户添加小部件后启动配置活动。用户仍然可以 重新配置小部件

使用 AppWidgetProvider 类处理小部件广播

AppWidgetProvider 类处理小部件广播,并根据小部件生命周期事件更新小部件。以下部分介绍如何在清单中声明 AppWidgetProvider,然后实现它。

在清单中声明小部件

首先,在您的应用程序的 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 属性,该属性指定小部件使用的 AppWidgetProvider。除非单独的进程需要向您的 AppWidgetProvider 广播,否则组件不得导出,这种情况通常不会发生。

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

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

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

实现 AppWidgetProvider 类

AppWidgetProvider 类扩展 BroadcastReceiver 作为方便类来处理小部件广播。它只接收与小部件相关的事件广播,例如当小部件更新、删除、启用和禁用时。当这些广播事件发生时,将调用以下 AppWidgetProvider 方法

onUpdate()
这在 AppWidgetProviderInfo 中由 updatePeriodMillis 属性定义的间隔内调用以更新小部件。有关更多信息,请参阅此页面中的 描述其他小部件属性的表格
当用户添加小部件时也会调用此方法,因此它会执行基本设置,例如定义 View 对象的事件处理程序或启动作业以加载数据以显示在小部件中。但是,如果您声明了没有 configuration_optional 标志的配置活动,则当用户添加小部件时,不会调用此方法,但在后续更新中调用此方法。配置活动的职责是在配置完成后执行第一次更新。有关更多信息,请参阅 启用用户配置应用小部件
最重要的回调是 onUpdate()。有关更多信息,请参阅此页面中的 使用 onUpdate() 类处理事件
onAppWidgetOptionsChanged()

当小部件第一次放置以及小部件大小调整时,会调用此方法。使用此回调根据小部件的大小范围显示或隐藏内容。通过调用 getAppWidgetOptions() 获取大小范围(以及从 Android 12 开始的小部件实例可以采用的可能大小列表),该方法返回一个包含以下内容的 Bundle

onDeleted(Context, int[])

每次从小部件主机删除小部件时都会调用此方法。

onEnabled(Context)

当第一次创建小部件实例时调用此方法。例如,如果用户添加了您的 widget 的两个实例,则此方法只在第一次调用。如果您需要打开一个新的数据库或执行其他仅需为所有小部件实例执行一次的设置,那么这是执行此操作的理想位置。

onDisabled(Context)

当您的小部件的最后一个实例从小部件主机中删除时,会调用此方法。这是您清理在 onEnabled(Context) 中完成的任何工作的地方,例如删除临时数据库。

onReceive(Context, Intent)

这在每次广播之前以及每个前面回调方法之前调用。通常您不需要实现此方法,因为默认的 AppWidgetProvider 实现会过滤所有小部件广播,并根据需要调用前面的方法。

您必须使用 AndroidManifest 中的 <receiver> 元素,将 AppWidgetProvider 类实现声明为广播接收器。有关更多信息,请参阅此页面中的 在清单中声明小部件

使用 onUpdate() 类处理事件

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

例如,如果您想要一个带有按钮的小部件,该按钮在点击时启动一个活动,您可以使用以下 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,该 PendingIntent 启动一个 Activity,并使用 setOnClickPendingIntent(int, PendingIntent) 将其附加到小部件的按钮。它包含一个循环,循环遍历 appWidgetIds 中的每个条目,appWidgetIds 是一个 ID 数组,标识由该提供程序创建的每个小部件。如果用户创建了 widget 的多个实例,那么它们都会同时更新。但是,只为 widget 的所有实例管理一个 updatePeriodMillis 计划。例如,如果更新计划定义为每两小时一次,并且在第一个 widget 之后一小时添加了第二个 widget 实例,那么它们都会在第一个定义的周期内更新,并且第二个更新周期会被忽略。它们每两小时更新一次,而不是每小时更新一次。

有关更多详细信息,请参阅 ExampleAppWidgetProvider.java 示例类。

接收 widget 广播意图

AppWidgetProvider 是一个便利类。如果您想直接接收 widget 广播,您可以实现自己的 BroadcastReceiver 或覆盖 onReceive(Context,Intent) 回调。您需要关注的意图如下:

创建 widget 布局

您必须在 XML 中定义 widget 的初始布局,并将其保存在项目的 res/layout/ 目录中。有关详细信息,请参阅 设计指南

如果您熟悉 布局,创建 widget 布局很简单。但是,请注意,widget 布局基于 RemoteViews,它不支持所有类型的布局或视图 widget。您不能使用自定义视图或 RemoteViews 支持的视图的子类。

RemoteViews 还支持 ViewStub,它是一个不可见的、大小为零的 View,您可以使用它在运行时延迟加载布局资源。

支持有状态行为

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

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

Example of shopping list widget showing stateful behavior
图 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));

提供两个布局:一个针对运行 Android 12 或更高版本的设备,位于 res/layout-v31 中,另一个针对之前的 Android 11 或更低版本,位于默认的 res/layout 文件夹中。

实现圆角

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

以下示例展示了一个使用 system_app_widget_background_radius 设置 widget 角,并使用 system_app_widget_inner_radius 设置 widget 内部视图角的 widget。

Widget showing radii of the widget background and views inside the widget
图 4. 圆角。

1 widget 的角。

2 widget 内部视图的角。

有关圆角的重要注意事项

  • 第三方启动器和设备制造商可以覆盖 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" />