提供灵活的部件布局

此页面描述了 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额外信息获取部件的大小范围,然后估算部件的大小,但此逻辑并非在所有情况下都有效。对于面向 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 属性所指定,或 180×110dp,如 minWidthminHeight 属性所指定(适用于运行 Android 11 或更低版本的设备)。在后一种情况下,单元格大小可能会因设备而异。

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

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

假设小部件使用前面代码片段中定义的自适应布局。这意味着从 180dp (minResizeWidth) x 110dp (minResizeHeight) 到 269x279dp(下一个临界点 - 1)使用指定为 R.layout.widget_weather_forecast_small 的布局。类似地,从 270x110dp 到 270x279dp 使用 R.layout.widget_weather_forecast_medium,从 270x280dp 到 530dp (maxResizeWidth) x 450dp (maxResizeHeight) 使用 R.layout.widget_weather_forecast_large

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

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