提供灵活的 widget 布局

尝试使用 Compose 方式
Jetpack Compose 是推荐用于 Android 的界面工具包。了解如何使用 Compose 风格的 API 构建 widget。

本页介绍了 Android 12(API 级别 31)中引入的 widget 大小调整改进和更出色的灵活性。本文还详细介绍了如何确定 widget 的尺寸

使用改进的 API 设置微件大小和布局

从 Android 12(API 级别 31)开始,您可以提供更精细的尺寸属性和灵活的布局,具体方法如下(如后续部分中所述):

  1. 指定额外的微件大小调整限制。

  2. 提供自适应布局精确布局

在之前的 Android 版本中,可以使用 OPTION_APPWIDGET_MIN_WIDTHOPTION_APPWIDGET_MIN_HEIGHTOPTION_APPWIDGET_MAX_WIDTHOPTION_APPWIDGET_MAX_HEIGHT extra 获取 widget 的大小范围,然后估计 widget 的大小,但该逻辑并非在所有情况下都适用。对于以 Android 12 或更高版本为目标平台的 widget,我们建议您提供自适应布局精确布局

指定额外的微件大小调整限制

Android 12 添加了一些 API,可让您确保微件在具有不同屏幕尺寸的不同设备上更可靠地调整大小。

除了现有的 minWidthminHeightminResizeWidthminResizeHeight 属性之外,还可以使用下面这些新的 appwidget-provider 属性:

以下 XML 展示了如何使用调整大小属性。

<appwidget-provider
  ...
  android:targetCellWidth="3"
  android:targetCellHeight="2"
  android:maxResizeWidth="250dp"
  android:maxResizeHeight="110dp">
</appwidget-provider>

提供自适应布局

如果布局需要根据微件的大小进行更改,我们建议您创建一小组布局,每个布局对一定范围的大小有效。如果无法做到这一点,另一种方法是根据运行时的精确 widget 大小来提供布局,如本页中所述。

此功能可使缩放更流畅并让系统的整体运行状况更好,因为系统不必每次以不同的大小显示微件时都唤醒应用。

以下代码示例展示了如何提供布局列表。

Kotlin

override fun onUpdate(...) {
    val smallView = ...
    val tallView = ...
    val wideView = ...

    val viewMapping: Map<SizeF, RemoteViews> = mapOf(
            SizeF(150f, 100f) to smallView,
            SizeF(150f, 200f) to tallView,
            SizeF(215f, 100f) to wideView
    )
    val remoteViews = RemoteViews(viewMapping)

    appWidgetManager.updateAppWidget(id, remoteViews)
}

Java

@Override
public void onUpdate(...) {
    RemoteViews smallView = ...;
    RemoteViews tallView = ...;
    RemoteViews wideView = ...;

    Map<SizeF, RemoteViews> viewMapping = new ArrayMap<>();
    viewMapping.put(new SizeF(150f, 100f), smallView);
    viewMapping.put(new SizeF(150f, 200f), tallView);
    viewMapping.put(new SizeF(215f, 100f), wideView);
    RemoteViews remoteViews = new RemoteViews(viewMapping);

    appWidgetManager.updateAppWidget(id, remoteViews);
}

假设 widget 具有以下属性:

<appwidget-provider
    android:minResizeWidth="160dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="200dp">
</appwidget-provider>

上述代码段的含义如下:

  • smallView 支持从 160dp (minResizeWidth) × 110dp (minResizeHeight) 到 160dp × 199dp(下一个截止点 - 1dp)的尺寸。
  • tallView 支持从 160dp × 200dp 到 214dp(下一个截止点 - 1)× 200dp 的尺寸。
  • wideView 支持的尺寸范围为 215dp × 110dp (minResizeHeight) 至 250dp (maxResizeWidth) × 200dp (maxResizeHeight)。

您的 widget 必须支持 minResizeWidth × minResizeHeightmaxResizeWidth × maxResizeHeight 的尺寸范围。在该范围内,您可以决定切换布局的截止点。

自适应布局示例
图 1. 自适应布局示例。

提供精确布局

如果一小组自适应布局不可行,您可以改为提供根据微件的显示大小量身定制的不同布局。通常,手机有两种大小(竖屏和横屏模式),可折叠设备有四种大小。

如需实现此解决方案,您的应用需要执行以下步骤:

  1. 过载 AppWidgetProvider.onAppWidgetOptionsChanged(),当一组大小发生更改时,就会调用此方法。

  2. 调用 AppWidgetManager.getAppWidgetOptions(),这样会返回包含大小的 Bundle

  3. 访问 Bundle 中的 AppWidgetManager.OPTION_APPWIDGET_SIZES 键。

以下代码示例展示了如何提供精确布局。

Kotlin

override fun onAppWidgetOptionsChanged(
        context: Context,
        appWidgetManager: AppWidgetManager,
        id: Int,
        newOptions: Bundle?
) {
    super.onAppWidgetOptionsChanged(context, appWidgetManager, id, newOptions)
    // Get the new sizes.
    val sizes = newOptions?.getParcelableArrayList<SizeF>(
            AppWidgetManager.OPTION_APPWIDGET_SIZES
    )
    // Check that the list of sizes is provided by the launcher.
    if (sizes.isNullOrEmpty()) {
        return
    }
    // Map the sizes to the RemoteViews that you want.
    val remoteViews = RemoteViews(sizes.associateWith(::createRemoteViews))
    appWidgetManager.updateAppWidget(id, remoteViews)
}

// Create the RemoteViews for the given size.
private fun createRemoteViews(size: SizeF): RemoteViews { }

Java

@Override
public void onAppWidgetOptionsChanged(
    Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
    super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
    // Get the new sizes.
    ArrayList<SizeF> sizes =
        newOptions.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES);
    // Check that the list of sizes is provided by the launcher.
    if (sizes == null || sizes.isEmpty()) {
      return;
    }
    // Map the sizes to the RemoteViews that you want.
    Map<SizeF, RemoteViews> viewMapping = new ArrayMap<>();
    for (SizeF size : sizes) {
        viewMapping.put(size, createRemoteViews(size));
    }
    RemoteViews remoteViews = new RemoteViews(viewMapping);
    appWidgetManager.updateAppWidget(id, remoteViews);
}

// Create the RemoteViews for the given size.
private RemoteViews createRemoteViews(SizeF size) { }

确定 widget 的尺寸

每个 widget 都必须为搭载 Android 12 或更高版本的设备定义 targetCellWidthtargetCellHeight,或者为所有版本的 Android 定义 minWidthminHeight,以指明其默认情况下占用的最小空间量。不过,当用户向其主屏幕添加微件时,微件占用的宽度和高度通常会超过您指定的最小值。

Android 主屏幕为用户提供了一个可用空间网格,供他们放置微件和图标。此网格可能因设备而异;例如,许多手机都提供 5x4 网格,而平板电脑则可提供更大的网格。添加微件后,它将在水平和垂直方向进行拉伸,占用满足其 targetCellWidthtargetCellHeight 约束条件(在搭载 Android 12 或更高版本的设备上)或 minWidthminHeight 约束条件(在搭载 Android 11 [API 级别 30] 或更低版本的设备上)所需的最小单元格数。

单元格的宽度和高度以及应用到 widget 的自动边距的大小可能会因设备而异。您可以使用下表根据所需占用的网格单元格数,大致估算微件在典型的 5x4 网格手机中的最小尺寸:

单元格数量(宽度 x 高度) 竖屏模式下的可用尺寸 (dp) 横屏模式下的可用尺寸 (dp)
1x1 57x102dp 127x51dp
2x1 130x102dp 269x51dp
3x1 203x102dp 412x51dp
4x1 276x102dp 554x51dp
5x1 349x102dp 697x51dp
5x2 349x220dp 697x117dp
5x3 349x337dp 697x184dp
5x4 349x455dp 697x250dp
n x m (73n - 16) x (118m - 16) (142n - 15) x (66m - 15)

使用竖屏模式下的单元格尺寸来确定您为 minWidthminResizeWidthmaxResizeWidth 属性提供的值。同样,您可以使用横屏模式下的单元格大小来确定为 minHeightminResizeHeightmaxResizeHeight 属性提供的值。

这是因为,在竖屏模式下,单元格宽度通常小于横屏模式下的单元格宽度;同样,在横屏模式下,单元格高度通常小于竖屏模式下的单元格高度。

例如,如果您希望 widget 宽度可调整为在 Google Pixel 4 上占据一个单元格,则需要将 minResizeWidth 设置为最多 56dp,以确保 minResizeWidth 属性的值小于 57dp,因为竖屏模式下的单元格宽度至少为 57dp。同样,如果您希望微件的高度在同一设备上可在一个单元格中调整大小,则需要将 minResizeHeight 设置为最多 50dp,以确保 minResizeHeight 属性的值小于 51dp,因为一个单元格在横屏模式下至少有 51dp 高。

每个 widget 都可以在 minResizeWidth/minResizeHeightmaxResizeWidth/maxResizeHeight 属性之间的尺寸范围内调整大小,这意味着它需要适应这两个属性之间的任何尺寸范围。

例如,如需设置放置时 widget 的默认大小,您可以设置以下属性:

<appwidget-provider
    android:targetCellWidth="3"
    android:targetCellHeight="2"
    android:minWidth="180dp"
    android:minHeight="110dp">
</appwidget-provider>

这意味着,微件的默认大小为 3x2 个单元格(由 targetCellWidthtargetCellHeight 属性指定),或者为 180×110dp(由 minWidthminHeight 针对搭载 Android 11 或更低版本的设备指定)。在后一种情况下,以单元格表示的大小可能会因设备而异。

此外,如需设置 widget 支持的大小范围,您可以设置以下属性:

<appwidget-provider
    android:minResizeWidth="180dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="530dp"
    android:maxResizeHeight="450dp">
</appwidget-provider>

如上述属性所指定,widget 的宽度可从 180dp 调整为 530dp,高度可从 110dp 调整为 450dp。然后,只要满足以下条件,该 widget 就可以调整大小,从 3x2 单元格调整为 5x2 单元格:

  • 设备采用 5x4 网格。
  • 单元格数量与以 dp 为单位的可用大小之间的对应关系遵循此页面中的显示最小尺寸估计值的表格
  • 微件会适应该尺寸范围。

Kotlin

val smallView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_small)
val mediumView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_medium)
val largeView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_large)

val viewMapping: Map<SizeF, RemoteViews> = mapOf(
        SizeF(180f, 110f) to smallView,
        SizeF(270f, 110f) to mediumView,
        SizeF(270f, 280f) to largeView
)

appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))

Java

RemoteViews smallView = 
    new RemoteViews(context.getPackageName(), R.layout.widget_weather_forecast_small);
RemoteViews mediumView = 
    new RemoteViews(context.getPackageName(), R.layout.widget_weather_forecast_medium);
RemoteViews largeView = 
    new RemoteViews(context.getPackageName(), R.layout.widget_weather_forecast_large);

Map<SizeF, RemoteViews> viewMapping = new ArrayMap<>();
viewMapping.put(new SizeF(180f, 110f), smallView);
viewMapping.put(new SizeF(270f, 110f), mediumView);
viewMapping.put(new SizeF(270f, 280f), largeView);
RemoteViews remoteViews = new RemoteViews(viewMapping);

appWidgetManager.updateAppWidget(id, remoteViews);

假设 widget 使用前面代码段中定义的自适应布局。这意味着,从 180dp (minResizeWidth) x 110dp (minResizeHeight) 到 269x279dp(下一个截止点 - 1),系统会使用指定为 R.layout.widget_weather_forecast_small 的布局。同样,R.layout.widget_weather_forecast_medium 用于 270x110dp 到 270x279dp,而 R.layout.widget_weather_forecast_large 用于 270x280dp 到 530dp (maxResizeWidth) x 450dp (maxResizeHeight)。

当用户调整 widget 大小时,其外观会发生变化,以适应每个单元格大小,如以下示例所示。

以最小的 3x2 网格尺寸显示的天气 widget 示例。界面会显示位置名称(东京)、温度 (14°) 和指示多云天气的符号。
图 2. 3x2 R.layout.widget_weather_forecast_small

尺寸为 4x2 的“中等”天气 widget 示例。以这种方式调整 widget 大小会基于之前 widget 大小的所有界面,并添加“多云”标签以及下午 4 点到晚上 7 点的温度预报。
图 3. 4x2 R.layout.widget_weather_forecast_medium

5x2“中等”尺寸的天气 widget 示例。以这种方式调整 widget 大小会产生与之前大小相同的界面,只不过它会拉伸一个单元格的长度,以占用更多横向空间。
图 4. 5x2 R.layout.widget_weather_forecast_medium

5x3“大”尺寸的天气 widget 示例。以这种方式调整 widget 大小会基于之前 widget 大小的所有界面,并在 widget 内添加一个视图,其中包含周二和周三的天气预报。指示晴天或雨天的符号,以及每天的最高温度和最低温度。
图 5. 5x3 R.layout.widget_weather_forecast_large

尺寸为 5x4 的“大”天气 widget 示例。以这种方式调整微件大小会沿用之前微件尺寸的所有界面,并添加周四和周五(以及它们对应的符号,用于指示天气类型以及每天的最高温度和最低温度)。
图 6. 5x4 R.layout.widget_weather_forecast_large