创建简单的 widget

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

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

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

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

微件组件

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

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

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

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

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

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

声明 AppWidgetProviderInfo XML

AppWidgetProviderInfo 对象定义了 widget 的基本特性。 在 XML 资源文件中使用单个AppWidgetProviderInfo <appwidget-provider> 元素并将其保存在项目的 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 尺寸的下限和上限。在这种情况下, 微件的默认大小是首次启动微件时,微件采用的尺寸。 已添加到主屏幕。

下表介绍了与 <appwidget-provider> 调整微件大小:

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

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

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

此微件的大小默认为 3x2。该 widget 可以缩小到 2x1 或 最高为全屏模式。

其他微件属性

下表介绍了与 <appwidget-provider> 例如微件大小调整

属性和说明
updatePeriodMillis 定义微件框架从 AppWidgetProvider(通过调用 onUpdate()) 回调方法。我们无法保证实际更新会在 该值,我们建议您尽可能降低更新频率 (最多每小时一次)来节省电量。 如需查看选择适当更新周期时的注意事项的完整列表, 请参阅 针对更新微件的优化 内容
initialLayout 指向用于定义 widget 布局的布局资源。
configure 定义用户添加 widget 时启动的 activity。 让他们可以配置 widget 属性。请参阅 允许用户配置微件。 从 Android 12 开始,您的应用可以跳过初始 配置。请参阅使用 微件的默认配置了解详情。
description 指定要为 微件。此元素在 Android 12 中引入。
previewLayout(Android 12) 和 previewImage(Android 11 及更低版本)
  • 从 Android 12 开始, previewLayout 属性指定可缩放的预览, 。理想情况下 作为该属性指定的布局 XML 与 具有实际默认值的实际 widget。
  • 在 Android 11 或更低版本中,previewImage 属性指定 widget 加载后的预览, 已配置,在选择应用 widget 时将看到。如果不是 则用户会看到应用的启动器图标。这个 字段对应于 android:previewImage 属性, <receiver> 元素中 AndroidManifest.xml 文件。
注意:我们建议您同时指定 previewImagepreviewLayout 属性,以便您的应用可以回退 如果用户的设备不支持,可以使用 previewImage previewLayout。有关详情,请参阅 具有可扩缩性的向后兼容性 微件预览
autoAdvanceViewId 指定自动推进的微件子视图的视图 ID 该 widget 的宿主。
widgetCategory 声明 widget 是否可以显示在主屏幕上 (home_screen)、锁定屏幕 (keyguard) 或 两者都有。对于 Android 5.0 及更高版本,只有 home_screen 有效。
widgetFeatures 声明 widget 支持的功能。例如,如果您希望 您的 widget 在用户添加该 widget 时使用其默认配置,指定 和 configuration_optionalreconfigurable 标志。这会绕过在用户使用应用后 添加该微件。用户仍然可以 重新配置 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> 元素必须包含一个带有<action> android:name 属性。此属性会指定 AppWidgetProvider 接受 ACTION_APPWIDGET_UPDATE 广播。这是您必须明确声明的唯一一项广播。通过 AppWidgetManager 自动将所有其他 widget 广播发送到 AppWidgetProvider,如下所示: 。

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

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

实现 AppWidgetProvider 类

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

onUpdate()
调用此方法可以按 AppWidgetProviderInfo 中的 updatePeriodMillis 属性。请参阅表格 其他微件属性
当用户添加 widget 时,也会调用此方法,以便执行 基本设置,例如为容器定义事件处理脚本, 要加载数据的 View 对象或启动作业 。不过,如果您声明了一个配置 activity,而没有 configuration_optional 标记之后,如果用户指定其值,系统不会调用此方法 会添加该 widget,但后续更新会调用它。它是 由配置 activity 负责执行首次更新, 配置完成。如需了解详情,请参阅允许用户配置应用微件
最重要的回调是 onUpdate()。请参阅使用 onUpdate()
onAppWidgetOptionsChanged()

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

onDeleted(Context, int[])

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

onEnabled(Context)

首次创建 widget 的实例时调用此方法。 例如,如果用户添加了微件的两个实例,则仅调用 第一次使用如果您需要打开一个新数据库或执行 所有微件实例只需要执行一次, 。

onDisabled(Context)

当 widget 的最后一个实例从 widget 托管应用。您可以在此处清理 onEnabled(Context) 中完成的所有工作。 例如删除临时数据库

onReceive(Context, Intent)

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

您必须将 AppWidgetProvider 类实现声明为广播 在 AndroidManifest 中使用 <receiver> 元素的接收器。请参阅声明 “微件”

使用 onUpdate() 类处理事件

最重要的 AppWidgetProvider 回调是 onUpdate(),因为它 会在每个 widget 添加到托管组件时调用,除非您使用配置 不含 configuration_optional 标志的 activity。如果微件接受 用户互动事件,然后在此回调中注册事件处理脚本。如果 您的微件不会创建临时文件或数据库,也不会执行其他工作 onUpdate() 可能是您需要清理的唯一回调方法 所需的定义

例如,如果您需要一个带有按钮的 widget,该按钮可在 则可以使用 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 并将其附加到 widget 的 按钮使用 setOnClickPendingIntent(int, PendingIntent)。它包含一个遍历每个条目的循环 appWidgetIds,这是 ID 数组,用于标识由 该提供商。如果用户创建了 widget 的多个实例,则 它们会同时更新但是,只有一个 updatePeriodMillis 时间表 对 widget 的所有实例进行管理。例如,如果更新时间表 定义为每两小时,并且会添加 widget 的第二个实例, 那么它们都会按照 并忽略第二个更新周期。它们每 2 次更新一次 而不是每小时

请参阅 ExampleAppWidgetProvider.java 示例类。

接收 widget 广播 intent

AppWidgetProvider 是一个辅助类。如果您想接收小程序 广播,因此您可以实现自己的 BroadcastReceiver 或替换 该 onReceive(Context,Intent) 回调。您需要关注的意图是 以下:

创建 widget 布局

您必须在 XML 中定义微件的初始布局,并将其保存在 项目的 res/layout/ 目录下。请参阅设计 指南了解详情。

如果您熟悉 layouts。但请注意 布局基于 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));

提供两种布局:一种针对的是搭载 Android 12 或 res/layout-v31,而另一个定位条件 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 的值。
  • 如果 widget 不使用 @android:id/background 或定义背景 使用 android:clipToOutline 根据大纲裁剪其内容, 设为 true - 启动器会自动识别背景和 使用圆角最大为 16 dp 的矩形裁剪 widget。 请参阅确保您的微件与 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" />