AppWidgetのサイズ事情

Android 12にてAndroidのウィジェットに大幅な強化が行われており、従来の知識では対応が不十分になっています。ここではサイズ事情の変化について説明しようと思います。

# API 16 〜 API 30

API 16（Android 4.1）から使える方法です。
サイズ情報は`getAppWidgetOptions()`で取得できるBundleから読み出すことができます。
以下のように読み出します。

```kotlin
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
val maxWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
val maxHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
```

サイズ情報はwidth/heightのmin/maxそれぞれの値として読み出します。
格納されている値はdpサイズですので、pixelサイズが必要な場合はdensity値との乗算が必要です。

意味合い的には、高さ幅それぞれの上限下限が与えられるので、その範囲で収まるレイアウトを決定することになります。
ただし、多くのホームアプリでは、minWidth x maxHeightがPortrait時、maxWidtgh x minHeightがLandscape時に割り当てられるウィジェットの正確なサイズが通知されており、これを元により正確なレイアウトを決定するという実装が行われる場合も多いかと思います。

注意点としてPortrait/Landscapeのサイズが格納されていると保証されているわけではないという点です。
これらの値はAndroidシステムとして保証されているものではなく、ホームアプリが設定するもののため、ホームアプリによってクセというか格納される値が異なっています。min/maxに全く同じ値が入っていたり、パディングを含むなどで、実際に表示されるサイズよりも大きかったり小さかったりする場合もあります。
これらは、ホームアプリから、ウィジェットを提供するアプリへ期待するサイズを格納しているだけ、程度に割り切る必要があります。

# API 31 〜

API 31（Android 12）以降、`OPTION_APPWIDGET_SIZES`というoptionsのKeyが追加されています。以下のように読み出します。

```kotlin
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
val sizes = BundleCompat.getParcelableArrayList(
    options, AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java
).orEmpty()
```

ArrayList<SizeF>が格納されています。従来のmin/maxではなく、より具体的な期待されるウィジェットのサイズ情報が伝えられるようになっています。一般的にはPortrait/Landscapeそれぞれのサイズが格納されています。
一部のFoldableデバイスの場合は、画面を折りたたんだ状態と開いた状態、それぞれのPortrait/Landscapeということで4つ以上のサイズが格納されている場合もあります。

Foldableデバイスが現れたことで、min/maxでは具体的なサイズ情報を伝えることができなくなっています。
2つ以上の期待されるウィジェットサイズを格納している場合、旧来のmin/maxの情報は文字通り、OPTION_APPWIDGET_SIZESに格納された縦横の最大最小サイズが格納されることになります。

## SizedRemoteViews

Foldableデバイス対応で複数のサイズを通知されるようになっていますが、RemoteViewsにも拡張が入っています

https://developer.android.com/reference/android/widget/RemoteViews#RemoteViews(java.util.Map%3Candroid.util.SizeF,android.widget.RemoteViews%3E)

API 31からRemoteViewsに`Map<SizeF, RemoteViews> remoteViews`を引数とするコンストラクタが追加されています。OPTION_APPWIDGET_SIZESで通知されたサイズと、それに対応するRemoteViewsをまとめて返せるようになっているのです。

画面の回転や折りたたみ、展開といった変化が発生した場合は、AppWidgetProviderから新しいRemoteViewsを作成するのでは無く、この中から最適なサイズのRemoteViewsを採用して表示できるようになっています。

なお、最適なサイズを検索するロジックは以下のようになっています。

https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=5990-6018

```java
private RemoteViews findBestFitLayout(@NonNull SizeF widgetSize) {
    // Find the better remote view
    RemoteViews bestFit = null;
    float bestSqDist = Float.MAX_VALUE;
    for (RemoteViews layout : mSizedRemoteViews) {
        SizeF layoutSize = layout.getIdealSize();
        if (layoutSize == null) {
            throw new IllegalStateException("Expected RemoteViews to have ideal size");
        }
        if (fitsIn(layoutSize, widgetSize)) {
            if (bestFit == null) {
                bestFit = layout;
                bestSqDist = squareDistance(layoutSize, widgetSize);
            } else {
                float newSqDist = squareDistance(layoutSize, widgetSize);
                if (newSqDist < bestSqDist) {
                    bestFit = layout;
                    bestSqDist = newSqDist;
                }
            }
        }
    }
    if (bestFit == null) {
        Log.w(LOG_TAG, "Could not find a RemoteViews fitting the current size: " + widgetSize);
        return findSmallestRemoteView();
    }
    return bestFit;
}
```

詳細はコードを読んでいただくとして、与えられたサイズの内側に収まるものの中で、縦横サイズ差の自乗和が最小のものを探しているようですね。どんなにサイズが近くてもオーバーすると選ばれないようです。

ただし、ホームアプリ側がこれらの変化に追従できていない場合もあるようで、SizedRemoteViewsを返すと適切なレイアウトが表示されずに崩れてしまう場合もあるようです。Jetpack Glanceを使った場合、SizedRemoteViewsがほぼ強制的に使われてしまうため、正確なサイズ情報に基づいたレスポンシブレイアウトを提供しようとすると、特定のホームアプリではレイアウトが崩れてしまうと言う問題に直面することになります。

---

以上です。