Provide flexible widget layouts

This page describes refinements for widget sizing and greater flexibility introduced with Android 12 (API level 31). It also covers detailed information about determining a size for your widget.

Use improved APIs for widget sizes and layouts

Starting in Android 12 (API level 31), you can provide more refined size attributes and more flexible layouts and by doing the following:

  1. Specifying additional widget sizing constraints

  2. Providing responsive layouts or exact layouts

In previous versions of Android, it was possible to get the size ranges of a widget using the OPTION_APPWIDGET_MIN_WIDTH, OPTION_APPWIDGET_MIN_HEIGHT, OPTION_APPWIDGET_MAX_WIDTH, and OPTION_APPWIDGET_MAX_HEIGHT extras and then estimate the widget's size, but that logic doesn't work in all situations. For widgets targeting Android 12, we recommend switching to providing responsive or exact layouts.

Specify additional widget sizing constraints

Android 12 adds new APIs allowing you to ensure your widget is sized more reliably across different devices with varying screen sizes.

In addition to the existing minWidth, minHeight, minResizeWidth, and minResizeHeight attributes, use the following new appwidget-provider attributes:

The following XML describes how to use the sizing attributes.

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

Provide responsive layouts

If the layout needs to change depending on the size of the widget, we recommend creating a small set of layouts, each valid for a range of sizes. If this isn’t possible, another option is to provide layouts based on the exact widget size at runtime as described in this document.

Implementing this feature allows for smoother scaling and overall better system health; this is because the system doesn't have to wake up the app every time it displays the widget in a different size.

The following code example shows how to provide a list of layouts.

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

Assuming the widget has the following attributes...

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

... the preceding code snippet means:

  • smallView supports from 160dp (minResizeWidth) × 110dp (minResizeHeight) to 150dp × 199dp (next cutoff point - 1dp).
  • mediumView supports from 150dp × 200dp to 249dp (next cutoff point - 1) × 200dp.
  • largeView supports from 215dp × 110dp (minResizeHeight) to 250dp (maxResizeWidth) × 190dp (maxResizeHeight).

Your widget must support the size range from minResizeWidth × minResizeHeight to maxResizeWidth × maxResizeHeight. Within that range, you can decide the cutoff point to switch layouts.

Example of responsive layout
Figure 1: Example of a responsive layout

Provide exact layouts

If a small set of responsive layouts isn't feasible, you can instead provide different layouts tailored to the sizes at which the widget is shown. This is typically two sizes for phones (portrait and landscape mode) and four sizes for foldables.

To implement this solution, your app needs to perform the following steps:

  1. Overload AppWidgetProvider#onAppWidgetOptionsChanged(...), which is called when the set of sizes changes.

  2. Call AppWidgetManager#getAppWidgetOptions(...), which returns a Bundle containing the sizes.

  3. Access the AppWidgetManager.OPTION_APPWIDGET_SIZES key from the Bundle.

The following code example shows how to provide exact layouts.

Kotlin

override fun onAppWidgetOptionsChanged(
        context: Context,
        manager: AppWidgetManager,
        id: Int,
        newOptions: Bundle?
) {
    super.onAppWidgetOptionsChanged(context, manager, 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)
}

// Creates 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<SizesF, RemoteViews> viewMapping = new ArrayMap<>();
    for (SizeF size : sizes) {
        viewMapping.put(size, createRemoteViews(size));
    }
    RemoteViews remoteViews = new RemoteViews(viewMapping);
    appWidgetManager.updateAppWidget(id, remoteViews);
}

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

Determine a size for your widget

Each widget must define a targetCellWidth and targetCellHeight (for devices running Android 12) or minWidth and minHeight (for all versions of Android), indicating the minimum amount of space it should consume by default. However, when users add a widget to their home screen, it generally occupies more than the minimum width and height you specify.

Android home screens offer users a grid of available spaces into which they can place widgets and icons. This grid can vary by device; for example, many handsets offer a 5x4 grid, and tablets can offer a larger grid. When your widget is added, it will be stretched to occupy the minimum number of cells, horizontally and vertically, required to satisfy constraints for its targetCellWidth and targetCellHeight (on devices running Android 12) or minWidth and minHeight constraints (on devices running Android 11 (API level 30) or lower).

Both the width and height of a cell and the amount of automatic margins applied to widgets may vary across devices. Use the following table to roughly estimate your widget's minimum dimensions in a typical 5x4 grid handset, given the number of occupied grid cells that you want:

Number of cells (width x height) Available size in portrait mode (dp) Available size in landscape mode (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)

You should use the portrait mode cell sizes to inform the values you provide for the minWidth, minResizeWidth, and maxRexizeWidth attributes. Similarly, you should use the landscape mode cell sizes to inform the values you provide for the minHeight, minResizeHeight, and maxResizeHeight attributes.

The reason for this is that the cell width is typically smaller in portrait mode than in in landscape mode—and similarly, the cell height is typically smaller in landscape mode than in portrait mode.

For example, if you’d like your widget width to be resizable down to one cell on an Google Pixel 4, you need to set your minResizeWidth to at most 56dp to make sure the value for the minResizeWidth attribute is smaller than 57dp (because a cell is at least 57dp wide in portrait). Similarly, if you want your widget height to be resizable in one cell on the same device, you need to set your minResizeHeight to at most 50dp to make sure the value for the minResizeHeight attribute is smaller than 51dp (because one cell is at least 51dp high in landscape mode).

Each widget is resizable within the size ranges between the minResizeWidth/minResizeHeight and maxResizeWidth/maxResizeHeight attributes, which means it needs to adapt to any size ranges between them.

For example, to set your default size of the widget on placement, you can set the following attributes:

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

This means the widget's default size is 3x2 cells as specified by the targetCellWidth and targetCellHeight attributes—or 180×110dp as specified by minWidth and minHeight for devices running Android 11 or lower. In this latter case, the size in cells may vary depending on the device.

Also, to set the supported size ranges of your widget, you can set the following attributes:

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

As specified by the preceding attributes, the width of the widget is resizable from 140dp to 530dp, and its height is resizable from 110dp to 450dp. The widget is then resizable from 3x2 to 5x2 cells, as long as the following conditions are present:

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

Let’s assume the widget uses the responsive layouts defined in the preceding code snippets. This means the layout specified as R.layout.widget_weather_forecast_small is used from 180dp (minResizeWidth) x 110dp (minResizeHeight) to 269x279dp (next cutoff points - 1). Similarly, R.layout.widget_weather_forecast_medium is used from 270x110dp to 270x279dp and R.layout.widget_weather_forecast_large is used from 270x280dp to 530dp (maxResizeWidth) x 250dp (maxResizeHeight).

As the user resizes the widget, its appearance changes to adapt to each size in cells, as shown in the following examples.

Example weather widget in the smallest 3x2-grid size. The UI shows
            the location name (Tokyo), temperature (14°), and symbol indicating
            partially cloudy weather.
Figure 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.
Figure 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.
Figure 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.
Figure 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).
Figure 6: 5x4 R.layout.widget_weather_forecast_large