提供灵活的 widget 布局

本页将介绍 Android 12(API 级别 31)中引入的微件大小调整机制和更高的灵活性。此外,还详细介绍了如何确定 widget 的大小

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

从 Android 12(API 级别 31)开始,您可以通过执行以下操作提供更精细的尺寸属性和灵活的布局,如以下部分中所述:

  1. 指定其他的 widget 大小调整限制。

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

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

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

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

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

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

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

提供自适应布局

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

此功能可实现更顺畅的缩放和更好的系统整体运行状况,因为系统不必在每次以不同尺寸显示 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 的大小

对于搭载 Android 12 或更高版本的设备,每个 widget 都必须定义 targetCellWidthtargetCellHeight;对于所有 Android 版本,每个 widget 都必须定义 minWidthminHeight,指明其默认占用的最小空间量。不过,当用户向其主屏幕添加 widget 时,其所占空间通常大于您指定的最小宽度和高度。

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

单元格的宽度和高度以及应用于 widget 的自动外边距大小可能因设备而异。根据下表,根据所需占用的网格单元数量,大致估算 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 米 (73n - 16) x (118m - 16) (142n - 15) x (66m - 15)

使用纵向模式单元格大小时,您需要告知为 minWidthminResizeWidthmaxResizeWidth 属性提供的值。同样,请使用横屏模式单元格大小来告知您为 minHeightminResizeHeightmaxResizeHeight 属性提供的值。

这是因为,纵向模式下的单元格宽度通常比横向模式下要小,同样,横向模式下的单元格高度通常比纵向模式下的单元格高度要小。

例如,如果您希望在 Google Pixel 4 上将 widget 宽度调整到一个单元格,则需要将 minResizeWidth 设置为最高 56dp,以确保 minResizeWidth 属性的值小于 57dp,因为一个单元格在纵向模式下至少为 57dp。同样,如果您希望在同一设备上的一个单元格中调整 widget 高度,则需要将 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>

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

此外,如需设置 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 为单位)之间的对应关系请参见本页中显示最小尺寸估算值的表格
  • widget 会适应该尺寸范围。

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 使用上述代码段中定义的自适应布局。这意味着,指定为 R.layout.widget_weather_forecast_small 的布局使用从 180dp (minResizeWidth) x 110dp (minResizeHeight) 到 269x279dp(下一个边缘点 - 1)的范围。同样,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“中”的天气微件示例。以这种方式调整微件的大小会在之前微件大小的所有界面的基础上调整,并添加了“大部分多云”标签以及下午 4 点到晚上 7 点的气温预测。
图 3. 4x2 R.layout.widget_weather_forecast_medium

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

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

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