提供灵活的小部件布局

此页面介绍了 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)。

您的 widget 必须支持从 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 网格,而平板电脑可以提供更大的网格。当添加您的 widget 时,它会拉伸以占用至少满足其targetCellWidthtargetCellHeight约束的水平和垂直单元数,或运行 Android 12 或更高版本的设备上的minWidthminHeight约束,或运行 Android 11(API 级别 30)或更低版本的设备上的约束。

单元格的宽度和高度以及应用于小部件的自动边距的大小可能会因设备而异。使用下表粗略估计您的 widget 在典型的 5x4 网格手机上的最小尺寸(以 dp 为单位),假设您要占用的网格单元数

单元格数 (宽度 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 个单元格:

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