创建简单微件

尝试 Compose 方式
Jetpack Compose 是推荐的 Android UI 工具包。了解如何使用 Compose 样式 API 构建微件。

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

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

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

有关如何设计微件的信息,请参阅应用微件概览

微件组件

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

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

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

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

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

  • 从 Android 12(API 级别 31)开始,您可以提供默认配置,并允许用户稍后重新配置微件。有关更多详细信息,请参阅使用微件的默认配置允许用户重新配置已放置的微件
  • 在 Android 11(API 级别 30)或更低版本中,用户每次将微件添加到主屏幕时,都会启动此 Activity。

我们还建议进行以下改进:灵活的微件布局杂项增强功能高级微件集合微件以及构建微件宿主

声明 AppWidgetProviderInfo XML

AppWidgetProviderInfo 对象定义了微件的基本特性。在一个 XML 资源文件中使用单个 <appwidget-provider> 元素定义 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 定义了用户添加微件时启动的 Activity,允许他们配置微件属性。请参阅允许用户配置微件。从 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 指定由微件宿主自动前进的微件子视图的 View ID。
widgetCategory 声明您的微件是否可以在主屏幕 (home_screen)、锁屏 (keyguard) 或两者上显示。对于 Android 5.0 及更高版本,只有 home_screen 有效。
widgetFeatures 声明微件支持的功能。例如,如果您希望微件在用户添加时使用其默认配置,请指定 configuration_optionalreconfigurable 标志。这会跳过在用户添加微件后启动配置 Activity。用户仍然可以在之后重新配置微件

使用 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()
调用此方法以按照 AppWidgetProviderInfoupdatePeriodMillis 属性定义的间隔更新微件。有关更多信息,请参阅此页面中描述附加微件属性的表
用户添加微件时也会调用此方法,因此它会执行必要的设置,例如为 View 对象定义事件处理程序或启动加载数据以在微件中显示的任务。但是,如果您声明的配置 Activity 没有 configuration_optional 标志,则用户添加微件时不会调用此方法,但会在后续更新时调用它。配置 Activity 的职责是在配置完成后执行首次更新。有关详细信息,请参阅允许用户配置应用微件
最重要的回调是 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(),因为它会在每个微件添加到宿主时调用,除非您使用的配置 Activity 没有 configuration_optional 标志。如果您的微件接受任何用户交互事件,则在此回调中注册事件处理程序。如果您的微件不创建临时文件或数据库,或执行其他需要清理的工作,则 onUpdate() 可能是您唯一需要定义的回调方法。

例如,如果您想要一个带按钮的微件,轻触该按钮时会启动一个 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,该 PendingIntent 会启动一个 Activity,并使用 setOnClickPendingIntent(int, PendingIntent) 将其附加到微件的按钮上。它包含一个循环,遍历 appWidgetIds 中的每个条目,appWidgetIds 是一个数组,其中包含此提供程序创建的每个微件的 ID。如果用户创建了多个微件实例,则它们会同时更新。但是,所有微件实例仅管理一个 updatePeriodMillis 调度。例如,如果更新调度定义为每两小时更新一次,并且在第一个微件实例添加一小时后添加了第二个微件实例,则它们都将按照第一个微件定义的周期更新,第二个更新周期将被忽略。它们都将每两小时更新一次,而不是每小时更新一次。

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

接收微件广播 Intent

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

创建微件布局

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

如果您熟悉布局,创建微件布局就很简单。但是,请注意微件布局基于 RemoteViews,它不支持每种布局或 View 微件。您不能使用自定义 View 或 RemoteViews 支持的 View 的子类。

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:微件背景的圆角半径,永不超过 28 dp。

  • 内半径,可以根据外半径和内边距计算。请参阅以下代码段

    /**
     * Applies corner radius for views that are visually positioned [widgetPadding]dp inside of the
     * widget background.
     */
    @Composable
    fun GlanceModifier.appWidgetInnerCornerRadius(widgetPadding: Dp): GlanceModifier {
    
        if (Build.VERSION.SDK_INT < 31) {
            return this
        }
    
        val resources = LocalContext.current.resources
        // get dimension in float (without rounding).
        val px = resources.getDimension(android.R.dimen.system_app_widget_background_radius)
        val widgetBackgroundRadiusDpValue = px / resources.displayMetrics.density
        if (widgetBackgroundRadiusDpValue < widgetPadding.value) {
            return this
        }
        return this.cornerRadius(Dp(widgetBackgroundRadiusDpValue - widgetPadding.value))
    }

要为微件的内部内容计算合适的半径,请使用以下公式:systemRadiusValue - widgetPadding

将其内容裁剪为非矩形形状的微件应使用 @android:id/background 作为背景 View 的 View ID,该背景 View 的 android:clipToOutline 设置为 true

圆角的重要注意事项

  • 第三方启动器和设备制造商可以覆盖 system_app_widget_background_radius 参数,使其小于 28 dp。
  • 如果您的微件不使用 @android:id/background 或不定义一个根据轮廓裁剪其内容的背景(将 android:clipToOutline 设置为 true),则启动器会自动识别背景并使用设置为系统半径的圆角矩形裁剪微件。

  • 非矩形形状需要包含在其圆角矩形调整大小容器内,以免被裁剪。

  • 从 Android 16 开始,system_app_widget_background_radius 的 AOSP 系统值为 24dp。启动器和设备制造商可以将微件裁剪到 system_app_widget_background_radius

  • 微件的内部内容必须具有足够的内边距,以支持最大 28dpsystem_app_widget_background_radius 半径值,以避免内容被圆角裁剪。

为了使微件兼容以前的 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" />