【Android】画面分割やカットアウトに負けない画面サイズの測り方

この記事は[and factory.inc Advent Calendar 2021](https://qiita.com/advent-calendar/2021/andfactory)　19日目の記事です。
昨日は @ykkd さんの [【iOS】ストレージの空き容量を取得する方法 & 容量不足のデバッグ](https://qiita.com/ykkd/items/a75a1a97cd72b3bbc469) でした。

# はじめに

Android12Lの発表やそれに関する[ドキュメント](https://developer.android.com/guide/topics/large-screens/migrate-to-responsive-layouts#things-to-avoid
)にもある通り、Androidアプリは様々な画面構成に対応する必要性が増しているのを感じます。
端末サイズの種類自体もそうですが、画面分割によってアプリの領域が多種多様に変化するのもその要因の一つです。

この記事では、そんな様々な画面構成の中で、アプリが使える領域を正確に測る方法について書こうと思います。

# 画面サイズの取得方法

結論から提示しますと、以下のソースで画面回転や画面分割、カットアウト表示等に対応した「アプリが描画可能な領域サイズ」を取得できます。

```kotlin
val wm = activity.windowManager
// contextから取得する場合
// val wm = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager

// 現在のウインドウサイズ
val bounds = wm.currentWindowMetrics.bounds
// 省かなければならない領域サイズ
val insetsIgnores = wm.currentWindowMetrics.windowInsets.getInsetsIgnoringVisibility(
        WindowInsets.Type.statusBars() or
        WindowInsets.Type.navigationBars() or
        WindowInsets.Type.displayCutout()
)

// 結果
val width = bounds.width() - (insetsIgnores.right + insetsIgnores.left)
val height = bounds.height() - (insetsIgnores.top + insetsIgnores.bottom)
```

これは主に [WindowMetrics.getBounds()](https://developer.android.com/reference/kotlin/android/view/WindowMetrics?hl=ja#getbounds) のドキュメントを参考にしています。

`getInsetsIgnoringVisibility(...)` は指定したTypeに沿ったサイズを[Insets](https://developer.android.com/reference/kotlin/android/graphics/Insets.html)の形式で取得できます。
コードの通り、right,left,top,bottomの4方向の値で各サイズを計算できます。
各Type別々に取得することもできますが、 `or` でまとめて指定することで、重なりを考慮した最終的な値が取得できます。

指定しているTypeは以下の三種類です。

- ステータスバー
- ナビゲーションバー
- ディスプレイのカットアウト（ノッチ）

ナビゲーションバーやカットアウトは、端末の向きやユーザ設定によって配置位置やサイズが異なることがありますが、
その都度取得したInsetsの値に反映されているので、これを再度考慮する必要はありません。

ただし、これだけでは完璧とは言えず、場合によって考慮しなければならない点が幾つかあります。

## 全画面モードへの対応
ステータスバーやナビゲーションバーを非表示するモードにする場合、注意が必要です。

[getinsetsignoringvisibility()](https://developer.android.com/reference/kotlin/android/view/WindowInsets#getinsetsignoringvisibility) の説明にありますが、これで取得できるサイズは「表示した場合のサイズ」になります。
つまり、全画面モードでステータスバーを非表示にしても、**取得できる値は0にはなりません。**

とはいえ、全画面モードにしているかは明示的に実装して分かっているはずなので、ステータスバーの計算をInsetに含めるかをその都度見直せば、十分対処できる問題です。

## API level30未満の場合
`currentWindowMetrics` はAPIlevel30以上を要求されます。
これも[WindowMetrics.getBounds()](https://developer.android.com/reference/kotlin/android/view/WindowMetrics?hl=ja#getbounds) のドキュメントにありあますが、基本としては以下の方法で取得することになります。

```kotlin
val point = Point()
wm.defaultDisplay.getSize(point) // Deprecated
val width = point.x
val height = point.y
```

また、この方法だとナビゲーションバーは含まれませんが、ステータスバーは含まれてしまいます。
こちらは古い取得方法なので、これ以上の詳細や注意点はここでは言及しません。

# 画面分割や回転時の挙動に関してのTips
サイズ取得に関してはこれまでの対応でほぼ十分に行えると思います。
ここでは、各要素の画面分割や回転時の挙動に関するTipsを書いていきます。

## カットアウトについて
まずこの「カットアウト」を聞き慣れない方も居るかと思いますが、前面カメラが画面に食い込む形で配置されている、一般的に「ノッチ」と呼ばれる領域のことです。
Androidでは公式ドキュメントでこの領域のことを[「カットアウト」](https://developer.android.com/guide/topics/display-cutout)と呼んでいます。

機種によって場所や形が様々ですが、端末の向きによって画面領域に異なる影響を及ぼします。

- カットアウトが画面上部：ステータスバーに食い込む、ステータスバーの高さがカットアウトの高さに引っ張られる
- カットアウトが画面左右：カットアウト部分の縦一列が描画されなくなる
- カットアウトが画面下部：ナビゲーションバーがカットアウトの高さ分持ち上げられる
  - 下部は上下逆にして表示できる端末でしか再現できない模様です

カットアウトがある端末を持っていなくても、開発者オプションの「ディスプレイのカットアウト」で試すことが可能です。ぜひ一度試してみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/10a88585-6425-75f6-14bb-61aff2389ec7.png" width="30%">

## ナビゲーションバーについて
ユーザのシステム設定によって、2〜3ボタンのナビゲーションの場合とジェスチャーナビゲーションの場合があるのはご存知かと思います。
これらは画面分割や画面回転した時に、それぞれ異なる配置になる可能性があるため、前述のInsetsの計算に影響があります。
実際に試してみると分かりやすいのですが、分割画面で画面回転すると、表示されてる方向によってナビゲーションバーの値が取得できたりできなかったりします。（後述のスクリーンショット参考）

# 実際に値を取得した例

最後に、これまでの説明を元に、bounds、各insets、計算結果の値をそれぞれ出力してみたキャプチャーを貼ります。
これを見れば、これまでの説明が具体的に実感できるのではないでしょうか。
（文字数が多く見辛いと思いますので、原寸に拡大して確認してください）

- Pixel3、素の状態
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/1630e111-30d8-b4e6-6991-456600b4777b.png" width="30%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/2b827dce-9c9a-c28d-0f1c-79d1ac525deb.png" width="50%">

- Pixel3、画面分割
    - ナビゲーションバーの取得サイズが場所によって異なることに注目

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/ba3b2f72-b24a-35b6-5f24-52ffd3d4d7f1.png" width="30%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/6b96ae6d-f0f2-6660-c289-faba50b38556.png" width="50%">

- Pixel3、画面分割＋ジェスチャーナビゲーション
    - ボタンナビゲーションとの違いに注目

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/484e0383-64d7-3590-c22c-03a14c7d1a22.png" width="30%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/0136ab73-14f9-7380-8c95-93cf99dce730.png" width="50%">

- iPlay_40H、画面端カットアウト（開発者オプション）＋画面分割、４方向
    - 上下反転ができるタブレット端末で確認
    - 縦方向の右上（上下反転だと左下）にカットアウトがある状態
        - スクリーンショットにはカットアウト範囲が描画されませんが、実機だと黒く描画されます
    - ディスプレイカットアウトの位置がすべて違うことに注目

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/6fc36887-54f1-af46-2d1b-8f8f87f69580.png" width="30%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/ee287cfc-659c-b329-28ee-b92c7424afbd.png" width="30%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/a459bd9f-8ec2-5a03-f260-d81f83597bd1.png" width="40%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/9ca422a6-b456-81c8-60ba-b903ebef713c.png" width="40%">

# 参考資料

- Android12L,画面構成の柔軟性に関するドキュメント

https://developer.android.com/guide/topics/large-screens/migrate-to-responsive-layouts#things-to-avoid

- ディスプレイカットアウト、Insetsに関するドキュメント
    - 残念ながら日本語ドキュメントは一部古いかページ自体がありません

https://developer.android.com/guide/topics/display-cutout?hl=ja

https://developer.android.com/training/gestures/edge-to-edge

- リファレンス関連

https://developer.android.com/reference/kotlin/android/view/WindowInsets#getinsetsignoringvisibility

https://developer.android.com/reference/kotlin/android/view/WindowMetrics?hl=ja#getbounds