提供灵活的小部件布局

本页面介绍了 Android 12(API 级别 31)中引入的小部件尺寸调整优化和更强的灵活性。它还详细说明了如何确定小部件的尺寸

使用改进的 API 设置小部件尺寸和布局

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

  1. 指定额外的小部件尺寸约束。

  2. 提供响应式布局精确布局

在早期版本的 Android 中,可以使用 OPTION_APPWIDGET_MIN_WIDTHOPTION_APPWIDGET_MIN_HEIGHTOPTION_APPWIDGET_MAX_WIDTHOPTION_APPWIDGET_MAX_HEIGHT extra 获取小部件的大小范围,然后估算小部件的尺寸,但这在所有情况下都不适用。对于以 Android 12 或更高版本为目标平台的小部件,我们建议提供响应式布局精确布局

指定额外的小部件尺寸约束

Android 12 添加了 API,让您能够更可靠地确保您的小部件在屏幕尺寸不同的各种设备上的尺寸一致性。

除了现有的 minWidthminHeightminResizeWidthminResizeHeight 属性外,请使用以下新的 appwidget-provider 属性

以下 XML 展示了如何使用尺寸属性。

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

提供响应式布局

如果布局需要根据小部件的尺寸进行更改,我们建议创建一组少量布局,每个布局适用于一个尺寸范围。如果这不可行,另一种选择是根据运行时的小部件精确尺寸提供布局,如本页面所述。

此功能可实现更流畅的缩放和整体更好的系统健康状态,因为系统不必在每次以不同尺寸显示小部件时唤醒应用。

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

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);
}

假设小部件具有以下属性

<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)的尺寸。

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

Example of responsive layout
图 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) { }

确定小部件的尺寸

对于运行 Android 12 或更高版本的设备,每个小部件必须定义 targetCellWidthtargetCellHeight;对于所有版本的 Android,则必须定义 minWidthminHeight,以指明默认情况下占用的最小空间量。但是,当用户将小部件添加到主屏幕时,它通常会占用超出您指定的最小宽度和高度的空间。

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

单元格的宽度和高度以及应用于小部件的自动边距大小可能因设备而异。请使用下表大致估算在典型的 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 属性提供的值。

这样做的原因是,单元格的宽度在纵向模式下通常比横向模式下小,类似地,单元格的高度在横向模式下通常比纵向模式下小。

例如,如果您希望在 Google Pixel 4 上将小部件宽度调整到最小为一个单元格,则需要将 minResizeWidth 设置为最多 56dp,以确保 minResizeWidth 属性的值小于 57dp(因为在纵向模式下,一个单元格的宽度至少为 57dp)。同样,如果您希望在同一设备上将小部件高度调整到最小为一个单元格,则需要将 minResizeHeight 设置为最多 50dp,以确保 minResizeHeight 属性的值小于 51dp(因为在横向模式下,一个单元格的高度至少为 51dp)。

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

例如,要在放置时设置小部件的默认尺寸,您可以设置以下属性

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

这意味着小部件的默认尺寸为 3x2 单元格,如 targetCellWidthtargetCellHeight 属性所指定,或者对于运行 Android 11 或更低版本的设备,尺寸为 180×110dp,如 minWidthminHeight 所指定。在后一种情况下,以单元格为单位的尺寸可能会因设备而异。

此外,要设置您的小部件支持的尺寸范围,您可以设置以下属性

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

如上面的属性所指定,小部件的宽度可从 180dp 调整到 530dp,高度可从 110dp 调整到 450dp。只要满足以下条件,小部件即可从 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);

假设小部件使用上面代码片段中定义的响应式布局。这意味着将使用 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)。

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

Example weather widget in the smallest 3x2-grid size. The UI shows
            the location name (Tokyo), temperature (14°), and symbol indicating
            partially cloudy weather.
图 2. 3x2 R.layout.widget_weather_forecast_small

Example weather widget in a 4x2 'medium' size. Resizing the widget
            this way builds on all of the UI from the previous widget size,
            and adds the label 'Mostly cloudy' and a forecast of temperatures from
            4pm through 7pm.
图 3. 4x2 R.layout.widget_weather_forecast_medium

Example weather widget in a 5x2 'medium' size. Resizing the widget
            this way results in the same UI as the previous size, except it is
            stretched by one cell length to take up more horizontal space.
图 4. 5x2 R.layout.widget_weather_forecast_medium

Example weather widget in a 5x3 'large' size. Resizing the widget
            this way builds on all of the UI from the previous widget sizes,
            and adds a view inside the widget containing a forecast of the weather
            on Tuesday and Wednesday. Symbols indicating sunny or rainy weather
            and high and low temperatures for each day.
图 5. 5x3 R.layout.widget_weather_forecast_large

Example weather widget in a 5x4 'large' size. Resizing the widget
            this way builds on all of the UI from the previous widget sizes,
            and adds Thursday and Friday (and their corresponding symbols
            indicating the type of weather as well as high and low temperature
            for each day).
图 6. 5x4 R.layout.widget_weather_forecast_large