見出し画像

システムUIに適合するためのComposeのAPI

REALITY Androidのテックリードをしているメタルおじさんです。

Compose 1.2でシステムUIの大きさに合わせたパディングを設定するためのModifierの拡張メソッドが追加されました。

  • captionBarPadding

  • displayCutoutPadding

  • imePadding

  • mandatorySystemGesturesPadding

  • navigationBarsPadding

  • safeContentPadding

  • safeDrawingPadding

  • safeGesturesPadding

  • statusBarsPadding

  • systemBarsPadding

  • systemGesturesPadding

  • waterfallPadding

えっ多すぎ…
一体どれを使えばいいんだ…
と思ったので、それぞれがどのような目的で使われるのかを調べてみました。


まず、これらの「xxxPadding」は、大きく以下の3種類に分類されます。

  • システムUIと視覚的に干渉する領域を避けるためのパディング

  • システムのジェスチャー操作と干渉する領域を避けるためのパディング

  • ディスプレイが物理的に特殊な形状をしていたり、完全な表示ができない可能性がある領域を避けるためのパディング

以下、詳しくみていきます。

システムUIと視覚的に干渉する領域

以下は、Android OSが描画するUI(システムUI)と視覚的に干渉する領域を避けるためのパディングを設定します。大抵のケースでこのカテゴリのパディングを使うことになるのではないかと思います。

statusBarsPadding

端末上部のシステムバーが占める領域分のパディングを付けます。
背景のUIはシステムバーの後ろまで描画したいが、その上に重なるUIはステータスバーの高さを考慮した位置に表示したい、という時に使えると思います。
もしくは、画面の一部分だけをComposeViewで作りたい場合に、画面上部のシステムUIのみを考慮したい(ナビゲーションバーのことは考慮しなくて良い)場合にも使えると思います。

navigationBarsPadding

端末下部(ではない端末もある)のナビゲーションバーが占める領域分のパディングを付けます。
背景はナビゲーションバーの後ろまで描画したいが、その上に出すUIはナビゲーションバーの高さを考慮した位置に表示したい、という時に使えると思います。

captionBarPadding

WindowInsetsCompat.Type.captionBar()で取得されるシステムUI領域分のパディングを付けます。
(REALITYではまだこのパディングが必要になるUIが作られていないため、詳しく調べられませんでした…)

systemBarsPadding

システムUIが占める領域全てのパディングを付けます。上記に上げたstatusBarsPadding, navigationBarsPadding, captionBarPaddingを全てを適用します。
Composeで一画面丸々を実装する際に、最上位となるComposable関数に付けるのが主な用途だと思います。

statusBarsPadding, navigationBarsPadding, systemBarsPaddingによりそれぞれどのようなパディングが設定されるか試してみました。
わかりやすいように、システムバー・ナビゲーションバーを半透明の黒 #6000 に設定しています。

statusBarsPadding()では上部のパディングが付くが、下部のパディングは付いていない
navigationBarsPadding()では下部のパディングが付くが、上部のパディングは付いていない
systemBarsPadding()では両方パディングが付いている

imePadding

ソフトウェアキーボードが占める領域分のパディングを付けます。
画面の下部にTextFieldを配置するようなUIで、キーボードを表示した時にキーボードの裏に入力中のテキストエリアが隠れないようにしたい、という時に使えると思います。

画面下部に配置したテキスト入力エリアにimePaddingを付けた例。
キーボードの表示・非表示に合わせて滑らかにアニメーションしてくれる。

これを正しく機能させるためにはAndroidManifest.xmlでandroid:windowSoftInputMode="adjustResize"を指定しておく必要があると思います。

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:windowSoftInputMode="adjustResize"
  >
  ...
</activity>

これを入れないと、キーボードの高さ分のパディングがActivity自体にもシステムによって付けられ、結果的に二重のパディングが設定されてしまうことになります。

imePadding()を使ってもなんか意図しない動作をするな?と思ったら
android:windowSoftInputMode="adjustResize"を確認しましょう

safeDrawingPadding

システムバーの他、ディスプレイカットアウト(後述)やソフトウェアキーボードで覆い隠される可能性のある領域全てのパディングを適用します。
これもimePadding()と同様、正しく機能させるためにはAndroidManifest.xmlでandroid:windowSoftInputMode="adjustResize"を指定しておく必要があります。

システムのジェスチャー操作と干渉する領域

OSがアプリより優先してジェスチャーを処理する可能性のある領域があります。たとえばフルスクリーンを解除するための上からのスワイプや、ジェスチャーナビゲーションの左右端からのスワイプなどです。

この領域を避けてUI要素を配置したい場合は、以下のパディングが役に立ちます。

systemGesturesPadding

WindowInsetsCompat.Type.systemGestures()で取得される領域にパディングを設定します。

たとえばフルスクリーンでステータスバーを非表示にしている時にはstatusBarsPadding()を付けてもパディングが設定されず、UIが画面上部にピッタリくっついて表示されます。
見た目には問題なくても、このUIを触ろうとすると「システムバーを再表示するための上からのスワイプ」を検出するためにOSが優先的にタッチイベントを処理してしまうため、アプリでうまくタッチイベントが処理できない可能性があります。
このような場合は、systemGesturesPadding()を使うと良いです。

フルスクリーンの画面だとsystemGesturesPadding()が役に立つ場合がある

safeGesturesPadding

systemGesturesPaddingの領域に加えて、WindowInsetsCompat.Type.mandatorySystemGestures()で取得される領域と、waterfallPaddingの領域(後述)を合わせたパディングを設定しているみたいです。systemGesturesPadding()では必要なパディングが不足している時に試すと良いのではないかと思います。
(が、これについてはまだ詳しく調べきれていません)

ディスプレイが物理的に特殊な形状をしている領域

最近のスマートフォンはディスプレイの一部分に穴が空いていてカメラが埋め込まれていたりして、ソフトウェア的に見えるディスプレイの領域全てが必ずしも実際に表示される領域ではなかったりします。そのような領域を避けてUI要素を配置するためのパディングが以下のものたちです。

displayCutoutPadding

Pixel 6など、ディスプレイの一部が切り抜かれてカメラが埋め込まれているような端末で、その切り抜き領域分のパディングを設定します(この部分のことをディスプレイカットアウトと呼びます)。
Pixel 4 XLのようにディスプレイカットアウトが存在しない端末では、パディングは設定されません。
REALITYでは今の所これが必要なUIにはまだ出会っていませんが、ディスプレイカットアウトを特別に考慮する必要があるUIの仕様に遭遇したら試してみてください。

ちなみに、ディスプレイカットアウトを持たない端末でも、開発者設定の中にあるディスプレイカットアウトを設定することで、ディスプレイカットアウトのある端末の挙動をシミュレートすることができます。

開発者向けオプション、知らないうちに項目増えてきたなぁ…

その他、詳しくはディスプレイ カットアウトのサポートのドキュメントもご覧ください。

waterfallPadding

ディスプレイの端にある曲がった領域のことをwaterfallと呼ぶそうです。(言われてみれば確かに滝のような形かもしれない)
その領域分のパディングを設定します。waterfallを持たない端末では、パディングは設定されません。

waterfall部分に表示されたUIはディスプレイが曲がっているために見えづらかったり、端末を握った時に意図せずタッチイベントを受け取ってしまったりするので、タッチ操作を目的としたUIや大事な情報を表示するUIを画面の端に置く時はこのパディングを付けると良いと思います。

…というはずなのですが、
試しに手元にあったwaterfallがあるPixel 6 ProでwaterfallPadding()を試してみましたが、画面両端のパディングは設定されませんでした…

Pixel 6 ProはwaterfallPadding()に対応していない?

注意点

paddingは消費される

親のComposableでパディングが設定された場合、そのパディング量は消費されます(原文のドキュメントではこのことをconsumedと書かれている)。
なので子のComposableで同じパディングを設定したとしても新たなパディングは設定されません。

Box(Modifier.statusBarsPadding()) { // A
  Box(Modifier.statusBarsPadding()) { // B
    Text("Hoge") // C
  }
}

上記のコードでAもBもstatusBarsPaddingを設定していますが、Aの時点でstatusBarsPaddingが消費されるため、Bのパディング量は0になり、Cはステータスバーの直下に描画されます。Cにステータスバーの高さ2個分のパディングが設定されるわけではないということです。

Edge-to-Edgeに描画するようになってないと動かない

ここまでに紹介したxxxPaddingは、アプリのUIをシステムUIの裏側にまで描画する設定が有効になっていないと動作しません(デフォルトではアプリのUIがシステムUIと被らないように自動的に調整されるため、そもそもComposeで描画するUIがシステムUIの大きさを知る必要がないですし)。
詳しくはAndroidのドキュメントのDisplay content edge-to-edge in your appのページをご覧ください。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Edge-to-Edgeに描画する設定
        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MyTheme {
                MyScreen(
                    modifier = Modifier.systemBarsPadding()
                )
            }
        }
    }
}

細かい制御をしたいときはWindowInsets

Modifier.xxxPaddingは、よく使いそうなパディングのプリセットです。しかし微妙にかゆいところに手が届かない場合があります。たとえば以下のような場合です。

  • systemGesturesPaddingのtopだけを適用したい

  • statusBarPaddingをステータスバーが非表示のときにも適用したい

こういうときはWindowInsetsを使用することで、細かい制御ができるようになります。

// systemGesturesPaddingのtopだけを適用
modifier = Modifier.padding(
  top = WindowInsets.systemGestures
    .asPaddingValues()
    .calculateTopPadding()
)

// statusBarPaddingをステータスバーが非表示のときにも適用
modifier = Modifier.padding(
  top = WindowInsets.statusBarsIgnoringVisibility
    .asPaddingValues()
    .calculateTopPadding()
)

ただし、一部のAPIは本記事執筆時点の1.2.0-rc01ではまだExperimentalとされているため、将来的に異なるAPIに置き換えられたりする可能性があります。

ComposeならシステムUIに適合するUIが簡単につくれる!

ViewベースのUI実装をしていた頃は、システムUIに適合するUIを作るのは大変でした。

  • android:fitsSystemWindows="true"をつければ良さそうかな?と思ったが、付けてもよくわからなかったり。

  • Kotlin(Java)コードでシステムUIの大きさを取得しようとしたらViewCompat.setOnApplyWindowInsetsListener を使わないといけなかったり(そしてこれがどういう挙動をするのかイメージしづらい)。

それが、Composeを使えば簡単に書けるようになります。Compose最高!

REALITYではComposeを使ったアプリ開発を一緒に推進してくれるAndroidエンジニアを募集しています。