构建应用微件托管应用

大多数 Android 设备上的 Android 主屏幕都允许用户嵌入应用微件,以便快速访问内容。如果您要构建主屏幕替代应用或类似的应用,还可以通过实现 AppWidgetHost 允许用户嵌入应用微件。大多数应用都不需要这样做,但如果您要创建自己的托管应用,请务必了解托管应用默许的约定义务。

本文档重点介绍实现自定义 AppWidgetHost 所涉及的责任。有关如何实现 AppWidgetHost 的示例,请参阅 Android 主屏幕启动器的源代码。

下面简要介绍了实现自定义 AppWidgetHost 所涉及的关键类和概念:

  • 应用微件托管应用 - AppWidgetHost 可以为要在界面中嵌入应用微件的应用(如主屏幕)提供与 AppWidget 服务的交互。AppWidgetHost 必须具有一个在托管应用自己的软件包中独一无二的 ID。此 ID 在托管应用的所有使用场合中保持不变。此 ID 通常是您在应用中分配的硬编码值。
  • 应用微件 ID - 每个应用微件实例在绑定时都分配有一个唯一的 ID(请参阅 bindAppWidgetIdIfAllowed()绑定应用微件部分对其进行了更详细的说明)。托管应用将使用 allocateAppWidgetId() 来获取此唯一 ID。此 ID 在微件的整个生命周期内(也就是说,直到从托管应用中将其删除)保持不变。任何特定于托管应用的状态(如微件的大小和位置)都应由托管软件包保留并与应用微件 ID 关联。
  • 应用微件托管应用视图 - 您可以将 AppWidgetHostView 看作一个框架,每当微件需要显示时,都会封装到该框架中。每当应用微件由托管应用扩充时,系统都会将微件分配给 AppWidgetHostView
  • 选项包 - AppWidgetHost 使用选项包将有关如何显示微件的信息(例如,大小范围以及微件是在锁定屏幕上还是在主屏幕上)传达给 AppWidgetProvider。利用此信息,AppWidgetProvider 可以根据微件的显示方式和显示位置来量身定制微件的内容和外观。您可以使用 updateAppWidgetOptions()updateAppWidgetSize() 来修改应用微件的选项包。这两种方法都会触发对 AppWidgetProvider 的回调。

绑定应用微件

当用户向托管应用添加应用微件时,会发生一个称为“绑定”的流程。绑定是指将特定应用微件 ID 与特定托管应用以及特定 AppWidgetProvider 关联。您可以通过多种不同的方法来做到这一点,具体取决于您的应用在哪个版本的 Android 系统上运行。

在 Android 4.0 及更低版本上绑定应用微件

在搭载 Android 4.0 及更低版本的设备上,用户通过一个系统 Activity 来添加应用微件,该 Activity 允许用户选择微件。这样会隐式执行权限检查 - 也就是说,通过添加应用微件,用户会隐式向您的应用授予将应用微件添加到托管应用的权限。以下示例(摘录自原始启动器)说明了此方法。在此代码段中,事件处理脚本使用请求代码 REQUEST_PICK_APPWIDGET 调用 startActivityForResult() 来响应用户操作:

Kotlin

    val REQUEST_CREATE_APPWIDGET = 5
    val REQUEST_PICK_APPWIDGET = 9
    ...
    override fun onClick(dialog: DialogInterface?, which: Int) {
        when (which) {
            ...
            AddAdapter.ITEM_APPWIDGET -> {
                ...
                val appWidgetId: Int = appWidgetHost.allocateAppWidgetId()
                val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                }
                ...
                startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
            }
            ...
        }
    }
    

Java

    private static final int REQUEST_CREATE_APPWIDGET = 5;
    private static final int REQUEST_PICK_APPWIDGET = 9;
    ...
    public void onClick(DialogInterface dialog, int which) {
        switch (which) {
        ...
            case AddAdapter.ITEM_APPWIDGET: {
                ...
                int appWidgetId =
                        Launcher.this.appWidgetHost.allocateAppWidgetId();
                Intent pickIntent =
                        new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK);
                pickIntent.putExtra
                        (AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
                ...
                startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET);
                break;
        }
        ...
    }
    

当该系统 Activity 完成时,它会将包含用户所选应用微件的结果返回给您的 Activity。在以下示例中,该 Activity 通过调用 addAppWidget() 来添加应用微件进行响应:

Kotlin

    class Launcher : Activity(), View.OnClickListener, View.OnLongClickListener {
        ...
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
            waitingFroResult = false

            if (resultCode == RESULT_OK && addItemCellInfo != null) {
                when (requestCode) {
                    ...
                    REQUEST_PICK_APPWIDGET -> addAppWidget(data)
                    REQUEST_CREATE_APPWIDGET ->
                        completeAddAppWidget(data, addItemCellInfo, !desktopLocked)
                    ...
                }
            }
            ...
        }
    }
    

Java

    public final class Launcher extends Activity
            implements View.OnClickListener, OnLongClickListener {
        ...
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            waitingForResult = false;

            if (resultCode == RESULT_OK && addItemCellInfo != null) {
                switch (requestCode) {
                    ...
                    case REQUEST_PICK_APPWIDGET:
                        addAppWidget(data);
                        break;
                    case REQUEST_CREATE_APPWIDGET:
                        completeAddAppWidget(data, addItemCellInfo, !desktopLocked);
                        break;
                    }
            }
            ...
        }
    }
    

addAppWidget() 方法会检查在添加应用微件之前是否需要先对其进行配置:

Kotlin

    fun addAppWidget(data: Intent?) {
        if (data != null) {
            val appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)

            val customWidget = data.getStringExtra(EXTRA_CUSTOM_WIDGET)
            val appWidget: AppWidgetProviderInfo? = appWidgetManager.getAppWidgetInfo(appWidgetId)

            appWidget?.configure?.apply {
                // Launch over to configure widget, if needed.
                val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
                intent.component = this
                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                startActivityForResult(intent, REQUEST_CREATE_APPWIDGET)
            } ?: run {
                // Otherwise, finish adding the widget.
            }
        }
    }
    

Java

    void addAppWidget(Intent data) {
        int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);

        String customWidget = data.getStringExtra(EXTRA_CUSTOM_WIDGET);
        AppWidgetProviderInfo appWidget =
                appWidgetManager.getAppWidgetInfo(appWidgetId);

        if (appWidget.configure != null) {
            // Launch over to configure widget, if needed.
            Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
            intent.setComponent(appWidget.configure);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            startActivityForResult(intent, REQUEST_CREATE_APPWIDGET);
        } else {
            // Otherwise, finish adding the widget.
        }
    }
    

如需详细了解配置,请参阅创建应用微件配置 Activity

应用微件准备就绪后,下一步是完成将其添加到工作区的实际工作。原始启动器使用一个名为 completeAddAppWidget() 的方法来执行此操作。

在 Android 4.1 及更高版本上绑定应用微件

Android 4.1 添加了一些 API 来使绑定流程更加简化。这些 API 还使托管应用能够提供用于绑定的自定义界面。要使用经过改进的这一流程,您的应用必须在其清单中声明 BIND_APPWIDGET 权限:

<uses-permission android:name="android.permission.BIND_APPWIDGET" />
    

但是,这只是第一步。在运行时,用户必须显式向您的应用授予权限,允许它将应用微件添加到托管应用。要测试您的应用是否具有添加微件的权限,您可以使用 bindAppWidgetIdIfAllowed() 方法。如果 bindAppWidgetIdIfAllowed() 返回 false,则您的应用必须显示一个对话框来提示用户授予权限(“允许”或“始终允许”,后者表示将来添加应用微件时一律允许)。以下代码段举例说明了如何显示该对话框:

Kotlin

    val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName)
        // This is the options bundle discussed above
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options)
    }
    startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
    

Java

    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName);
    // This is the options bundle discussed above
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
    startActivityForResult(intent, REQUEST_BIND_APPWIDGET);
    

托管应用还必须检查用户是否添加了需要配置的应用微件。如需详细了解此主题,请参阅创建应用微件配置 Activity

托管应用的责任

微件开发者可以使用 AppWidgetProviderInfo 元数据为微件指定一些配置设置。托管应用可以从与微件提供程序关联的 AppWidgetProviderInfo 对象检索这些配置选项(下文对此进行了更详细的说明)。

无论您的目标 Android 版本是哪个,所有托管应用都有以下责任:

  • 添加微件时,您必须按照上面的说明来分配微件 ID。您还必须确保在从托管应用中移除微件后调用 deleteAppWidgetId() 来取消分配相应的微件 ID。
  • 添加微件时,请务必启动其配置 Activity(如果存在),如通过配置 Activity 更新应用微件中所述。对于许多应用微件来说,这是一个必要的步骤,执行此步骤后,才能正确显示这些微件。
  • 每个应用微件都会指定最小宽度和高度(以 dp 为单位),如 AppWidgetProviderInfo 元数据中定义(使用 android:minWidthandroid:minHeight)。请确保微件的布局中至少包含这么多 dp。例如,许多托管应用在网格中将图标与微件对齐。在这种情况下,托管应用默认应使用满足 minWidthminHeight 约束条件的最小数量的单元格来添加应用微件。

除了上面列出的要求之外,特定平台版本引入的一些功能还要求托管应用承担新的责任。

您的目标版本是哪个?

您在实现托管应用时使用的方法应取决于您的目标 Android 版本。本部分中介绍的许多功能都是在 3.0 或更高版本中引入的。例如:

  • Android 3.0(API 级别 11)引入了微件的自动跳转行为。
  • Android 3.1(API 级别 12)引入了调整微件大小的功能。
  • Android 4.0(API 级别 15)引入了内边距策略的更改,更改后要求托管应用承担管理内边距的责任。
  • Android 4.1(API 级别 16)添加了一个 API,可让微件提供程序获取有关托管其微件实例的环境的更为详细的信息。
  • Android 4.2(API 级别 17)引入了选项包和 bindAppWidgetIdIfAllowed() 方法。此外,还引入了锁定屏幕微件。

如果您的目标设备搭载的是较低的版本,请以原始启动器为例加以参考。

下面几部分更为详细地介绍了一些要求托管应用承担新责任的功能。

Android 3.0

Android 3.0(API 级别 11)引入了让微件指定 autoAdvanceViewId() 的功能。此视图 ID 应指向 Advanceable 的实例,如 StackViewAdapterViewFlipper。这表示托管应用应按照它认为合适的时间间隔对此视图调用 advance()(考虑到跳转相应微件是否合理 - 例如,如果某个微件在其他页面上,或者如果屏幕已关闭,那么托管应用或许不希望跳转该微件)。

Android 3.1

Android 3.1(API 级别 12)引入了调整微件大小的功能。微件可以使用 AppWidgetProviderInfo 元数据中的 android:resizeMode 属性来指定它可调整大小,并指明它是否支持水平和/或垂直大小调整。此外,微件还可以指定 android:minResizeWidth 和/或 android:minResizeHeight,此功能是在 Android 4.0(API 级别 14)中引入的。

由托管应用负责按照微件的指定使其能够在水平和/或垂直方向上调整大小。对于指定自身可调整大小的微件,您可以将其大小调整为任意大,但不应将其大小调整为小于由 android:minResizeWidthandroid:minResizeHeight 指定的值。如需查看示例实现,请参阅 Launcher2 中的 AppWidgetResizeFrame

Android 4.0

Android 4.0(API 级别 15)引入了内边距策略的更改,更改后要求托管应用承担管理内边距的责任。从 4.0 开始,应用微件不再有自己的内边距,而是系统根据当前屏幕的特征来为每个微件添加内边距。这样可使微件在网格中显得更加统一和一致。为了协助托管应用微件的应用,我们的平台提供了 getDefaultPaddingForWidget() 方法。应用可调用此方法来获取系统定义的内边距,并在计算要分配给微件的单元格数量时考虑该内边距。

Android 4.1

Android 4.1(API 级别 16)添加了一个 API,可让微件提供程序获取有关托管其微件实例的环境的更为详细的信息。具体而言,托管应用会向微件提供程序提供提示,指出显示的微件大小是多大。由托管应用负责提供此大小信息。

托管应用将通过 updateAppWidgetSize() 提供此信息。系统会将大小指定为最小和最大宽度/高度(以 dp 为单位)。之所以指定范围(而不是固定大小),是因为微件的宽度和高度可能会随方向而发生变化。您肯定不希望托管应用必须轮流更新它的所有微件,因为这样可能会导致严重拖慢系统运行速度。当放置微件时、每当调整微件大小时,以及每当启动器在给定启动过程中首次扩充微件时,这些值都应更新一次(因为重新启动后不会保留这些值)。

Android 4.2

Android 4.2(API 级别 17)添加了在绑定时指定选项包的功能。这是指定应用微件选项(包括大小)的理想方式,因为它可让 AppWidgetProvider 在首次更新时立即访问选项数据。这可以通过使用 bindAppWidgetIdIfAllowed() 方法来实现。如需详细了解此主题,请参阅绑定应用微件

Android 4.2 还引入了锁定屏幕微件。在锁定屏幕上托管微件时,托管应用必须在应用微件选项包中指定此信息(AppWidgetProvider 可以使用此信息来适当地设置微件的样式)。要将微件指定为锁定屏幕微件,请使用 updateAppWidgetOptions() 并在 OPTION_APPWIDGET_HOST_CATEGORY 字段中添加 WIDGET_CATEGORY_KEYGUARD 值。此选项默认为 WIDGET_CATEGORY_HOME_SCREEN,因此没有明确要求为主屏幕托管应用设置此选项。

请确保托管应用仅添加适合应用的应用微件 - 例如,如果托管应用是主屏幕,请确保 AppWidgetProviderInfo 元数据中的 android:widgetCategory 属性包含 WIDGET_CATEGORY_HOME_SCREEN 标志。同样,对于锁定屏幕,请确保该字段包含 WIDGET_CATEGORY_KEYGUARD 标志。如需详细了解此主题,请参阅在锁定屏幕上启用应用微件