创建一个简单的部件

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

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

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

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

小部件组件

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

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

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

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

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

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

声明AppWidgetProviderInfo XML

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及更低版本中将被忽略,如果主屏幕不支持基于网格的布局,则可以被忽略。
  • minWidthminHeight属性以dp为单位指定小部件的默认大小。如果小部件的最小宽度或高度的值与单元格的尺寸不匹配,则这些值将向上舍入到最接近的单元格大小。
我们建议同时指定两组属性——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 为部件选择器指定要为您的部件显示的描述。在 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 声明您的部件是否可以在主屏幕 (home_screen)、锁屏 (keyguard) 或两者上显示。对于 Android 5.0 及更高版本,只有 home_screen 有效。
widgetFeatures 声明部件支持的特性。例如,如果您希望您的部件在用户添加它时使用其默认配置,则同时指定 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> 元素必须包含一个 <action> 元素,其中包含 android:name 属性。此属性指定 AppWidgetProvider 接受 ACTION_APPWIDGET_UPDATE 广播。这是您必须显式声明的唯一广播。AppWidgetManager 会根据需要自动将所有其他部件广播发送到 AppWidgetProvider

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

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

实现 AppWidgetProvider 类

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

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

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

onDeleted(Context, int[])

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

onEnabled(Context)

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

onDisabled(Context)

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

onReceive(Context, Intent)

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

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

使用 onUpdate() 类处理事件

最重要的 AppWidgetProvider 回调是 onUpdate(),因为除非您使用没有 configuration_optional 标志的配置活动,否则会在每个部件添加到主机时调用它。如果您的部件接受任何用户交互事件,则在此回调中注册事件处理程序。如果您的部件不创建临时文件或数据库,或执行需要清理的其他工作,则 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,启动一个Activity,并使用setOnClickPendingIntent(int, PendingIntent)将其附加到小部件的按钮上。它包含一个循环,迭代appWidgetIds中的每个条目,这是一个标识此提供程序创建的每个小部件的ID数组。如果用户创建了多个小部件实例,则它们将同时更新。但是,所有小部件实例都只管理一个updatePeriodMillis调度。例如,如果更新计划定义为每两小时一次,并且在第一个小部件添加一小时后添加了第二个小部件实例,则它们都将在第一个小部件定义的周期内更新,而第二个更新周期将被忽略。它们都每两小时更新一次,而不是每小时一次。

更多详情,请参阅ExampleAppWidgetProvider.java示例类。

接收小部件广播意图

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

创建小部件布局

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

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

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

支持有状态行为

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

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

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 引入了以下系统参数来设置小部件圆角的半径:

以下示例显示一个小部件,它使用system_app_widget_background_radius作为小部件的角,使用system_app_widget_inner_radius作为小部件内的视图。

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

1 小部件的角。

2 小部件内视图的角。

圆角的重要注意事项

  • 第三方启动器和设备制造商可以将system_app_widget_background_radius参数覆盖为小于 28 dp。 system_app_widget_inner_radius参数始终比system_app_widget_background_radius的值小 8 dp。
  • 如果您的窗口小部件不使用@android:id/background或不定义一个基于轮廓剪辑其内容的背景——android:clipToOutline设置为true——启动器会自动识别背景并使用具有最大 16 dp 圆角的矩形剪辑窗口小部件。请参阅确保您的窗口小部件与 Android 12 兼容

为了使小部件与之前的 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" />