Compose のウィンドウ インセット

Android プラットフォームは、ステータスバーやナビゲーション バーなどのシステム UI を描画します。このシステム UI は、ユーザーが使用しているアプリに関係なく表示されます。WindowInsets はシステム UI に関する情報を提供し、アプリが正しい領域に描画され、UI がシステム UI によって隠されないようにします。

システムバーの背後にエッジ ツー エッジで描画する
図 1. エッジ ツー エッジでシステムバーの背後に描画する

デフォルトでは、アプリの UI のレイアウトは、ステータスバーやナビゲーション バーなど、システム UI 内に制限されます。これにより、アプリのコンテンツがシステム UI 要素で隠れることがなくなります。

ただし、システム UI も表示されるこれらの領域への表示をオプトインすることをおすすめします。これにより、よりシームレスなユーザー エクスペリエンスが実現し、アプリで利用可能なウィンドウ スペースを最大限に活用できます。また、アプリをシステム UI と一緒にアニメーション化することもできます(特にソフトウェア キーボードの表示と非表示を切り替える場合)。

このような領域での表示を有効にして、システム UI の背後にコンテンツを表示することを「エッジ ツー エッジ」と呼びます。このページでは、インセットの種類、エッジ ツー エッジへのオプトイン方法、インセット API を使用して UI をアニメーション化し、アプリの各部分が見えにくくならないようにする方法について説明します。

インセットの基本

アプリをエッジ ツー エッジで使用する場合、重要なコンテンツやインタラクションがシステム UI によって隠れないようにする必要があります。たとえば、ボタンがナビゲーション バーの背後に配置されている場合、ユーザーがクリックできないことがあります。

システム UI のサイズと配置場所に関する情報は、インセットで指定します。

システム UI の各部分には、そのサイズと配置場所を示す、対応するタイプのインセットがあります。たとえば、ステータスバー インセットはステータスバーのサイズと位置を提供し、ナビゲーション バー インセットはナビゲーション バーのサイズと位置を提供します。各タイプのインセットは、上、左、右、下の 4 つのピクセル寸法で構成されます。これらのディメンションにより、システム UI がアプリ ウィンドウの対応する側面からどの程度まで伸びるかが決まります。したがって、このタイプのシステム UI との重複を避けるため、アプリ UI はその量で挿入する必要があります。

次に示す組み込みの Android インセット タイプは、WindowInsets を通じて利用できます。

WindowInsets.statusBars

ステータスバーを説明するインセット。通知アイコンやその他のインジケーターを含む上位のシステム UI バーです。

WindowInsets.statusBarsIgnoringVisibility

それらが表示される場合のステータスバー インセット。没入型全画面モードになったためにステータスバーが現在非表示になっている場合、メインのステータスバー インセットは空になりますが、これらのインセットは空になりません。

WindowInsets.navigationBars

ナビゲーション バーを説明するインセット。デバイスの左側、右側、下部のシステム UI バーで、タスクバーやナビゲーション アイコンを表します。これらは、ユーザーが選択したナビゲーション方法やタスクバーの操作に基づいて、実行時に変更される可能性があります。

WindowInsets.navigationBarsIgnoringVisibility

表示するタイミングを指定するナビゲーション バー インセット。没入型の全画面モードになったためにナビゲーション バーが現在非表示になっている場合、メインのナビゲーション バーのインセットは空になりますが、これらのインセットは空になりません。

WindowInsets.captionBar

トップ タイトルバーなど、フリーフォーム ウィンドウの場合のシステム UI ウィンドウの装飾を説明するインセット。

WindowInsets.captionBarIgnoringVisibility

字幕を表示する場合の字幕バー インセット。字幕バーが現在非表示の場合、メインの字幕バーのインセットは空になりますが、これらのインセットは空になりません。

WindowInsets.systemBars

システムバー インセット(ステータスバー、ナビゲーション バー、字幕バーを含む)を組み合わせたものです。

WindowInsets.systemBarsIgnoringVisibility

システムバーを表示する場合のインセット。没入型の全画面モードになったためにシステムバーが現在非表示になっている場合、メインのシステムバー インセットは空になりますが、これらのインセットは空になりません。

WindowInsets.ime

下部のソフトウェア キーボードが占有するスペースを表すインセット。

WindowInsets.imeAnimationSource

現在のキーボード アニメーションのにソフトウェア キーボードが占めていたスペースを表すインセット。

WindowInsets.imeAnimationTarget

現在のキーボード アニメーションのにソフトウェア キーボードが占有するスペースを表すインセット。

WindowInsets.tappableElement

ナビゲーション UI に関する詳細情報を記述するインセットの一種で、「タップ」がアプリではなくシステムが処理するスペースを指定します。ジェスチャー ナビゲーションを備えた透明なナビゲーション バーでは、一部のアプリ要素をシステム ナビゲーション UI からタップできます。

WindowInsets.tappableElementIgnoringVisibility

表示される場合のタップ可能な要素インセット。没入型全画面モードになったために、タップ可能な要素が現在非表示になっている場合、タップ可能なメインの要素のインセットは空になりますが、これらのインセットは空になりません。

WindowInsets.systemGestures

システムがナビゲーションのためにジェスチャーをインターセプトするインセットの量を表すインセット。アプリは Modifier.systemGestureExclusion を介して、これらの操作の一部の処理を手動で指定できます。

WindowInsets.mandatorySystemGestures

常にシステムによって処理され、Modifier.systemGestureExclusion でオプトアウトできないシステム ジェスチャーのサブセット。

WindowInsets.displayCutout

ディスプレイ カットアウト(ノッチまたはピンホール)との重複を避けるために必要な間隔を表すインセット。

WindowInsets.waterfall

ウォーターフォール ディスプレイの曲線領域を表すインセット。ウォーターフォール ディスプレイでは、画面の端に曲線領域があり、この領域で画面がデバイスの側面に沿って回り始めます。

これらのタイプは、コンテンツが不明瞭にならないように、3 つの「安全な」インセット タイプで要約されています。

これらの「安全な」インセット タイプは、基盤となるプラットフォーム インセットに基づいて、さまざまな方法でコンテンツを保護します。

  • システム UI の下に描画されてはならないコンテンツを保護するには、WindowInsets.safeDrawing を使用します。インセットの最も一般的な使用方法は、システム UI によって(部分的または全体的に)隠されたコンテンツが描画されないようにすることです。
  • WindowInsets.safeGestures を使用すると、ジェスチャーでコンテンツを保護できます。これにより、システム ジェスチャーとアプリのジェスチャー(ボトムシート、カルーセル、ゲーム内のジェスチャーなど)との競合を回避できます。
  • WindowInsets.safeContentWindowInsets.safeDrawingWindowInsets.safeGestures と組み合わせて、コンテンツに視覚的な重なりや操作の重複がないようにします。

インセットの設定

アプリがコンテンツを描画する場所を完全に制御できるようにするには、次のセットアップ手順を行います。この手順を行わないと、アプリでシステム UI の背後に黒や単色が描画されたり、ソフトウェア キーボードと同期してアニメーション化されなかったりすることがあります。

  1. Activity.onCreateenableEdgeToEdge() を呼び出します。この呼び出しは、アプリをシステム UI の背後に表示するようリクエストします。これにより、これらのインセットを使用して UI を調整する方法は、アプリで制御できます。
  2. アクティビティの AndroidManifest.xml エントリで android:windowSoftInputMode="adjustResize" を設定します。この設定により、アプリはソフトウェア IME のサイズをインセットとして受け取ることができます。これにより、IME がアプリに表示されたり消えたりしたときに、コンテンツを適切にパディングして配置できます。

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

Compose API

アクティビティがすべてのインセットの処理を管理できるようになったら、Compose API を使用して、コンテンツが不明瞭化されず、操作可能な要素がシステム UI と重ならないようにできます。これらの API は、アプリのレイアウトをインセットの変更と同期します。

たとえば、これはアプリ全体のコンテンツにインセットを適用する最も基本的な方法です。

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

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

このスニペットでは、アプリのコンテンツ全体の周囲のパディングとして safeDrawing ウィンドウ インセットを適用します。これにより、操作可能な要素がシステム UI と重ならないようになりますが、どのアプリもシステム UI の背後に描画されず、エッジ ツー エッジ効果を実現します。ウィンドウ全体を最大限に活用するには、画面ごと、またはコンポーネントごとにインセットを適用する場所を微調整する必要があります。

これらのインセット タイプはすべて、API 21 にバックポートされた IME アニメーションで自動的にアニメーション化されます。拡張機能により、これらのインセットを使用するすべてのレイアウトも、インセット値が変更されると自動的にアニメーション化されます。

これらのインセット タイプを使用してコンポーザブル レイアウトを調整する主な方法は、パディング修飾子とインセット サイズ修飾子の 2 つです。

パディング修飾子

Modifier.windowInsetsPadding(windowInsets: WindowInsets) は、指定されたウィンドウ インセットをパディングとして適用し、Modifier.padding と同様に動作します。たとえば、Modifier.windowInsetsPadding(WindowInsets.safeDrawing) と指定すると、安全な描画インセットが 4 辺すべてにパディングとして適用されます。

最も一般的なインセット タイプに対応する組み込みのユーティリティ メソッドもいくつかあります。Modifier.safeDrawingPadding() はそのようなメソッドの 1 つで、Modifier.windowInsetsPadding(WindowInsets.safeDrawing) と同等です。他のインセット タイプにも同様の修飾子があります。

インセットのサイズ修飾子

次の修飾子は、コンポーネントのサイズをインセットのサイズに設定することで、一定量のウィンドウ インセットを適用します。

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

windowInsets の開始側を幅として適用します(Modifier.width など)。

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

windowInsets の終端を幅として適用します(Modifier.width など)。

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

windowInsets の上側を高さとして適用します(Modifier.height など)。

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

windowInsets の下側を高さとして適用します(Modifier.height など)。

以下の修飾子は、インセットのスペースを占有する Spacer のサイズ設定に特に便利です。

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

インセット消費

インセットのパディング修飾子(windowInsetsPaddingsafeDrawingPadding などのヘルパー)は、パディングとして適用されるインセットの部分を自動的に消費します。コンポジション ツリーの奥深くに進むと、ネストされたインセット パディング修飾子とインセット サイズ修飾子は、インセットの一部が外部インセット パディング修飾子によって消費されていることを認識し、インセットの同じ部分を複数回使用しないようにします。

また、インセットのサイズ修飾子を使用すると、インセットがすでに使用されている場合、インセットの同じ部分を複数回使用しないこともできます。ただし、サイズを直接変更するので、インセット自体は消費しません。

そのため、パディング修飾子をネストすると、各コンポーザブルに適用されるパディングの量が自動的に変更されます。

前と同じ LazyColumn の例では、LazyColumnimePadding 修飾子によってサイズ変更されています。LazyColumn 内では、最後のアイテムのサイズがシステムバーの最下部の高さに設定されます。

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

IME が閉じているときは、IME に高さがないため、imePadding() 修飾子はパディングを適用しません。imePadding() 修飾子はパディングを適用していないため、インセットは消費されず、Spacer の高さはシステムバーの下側のサイズになります。

IME を開くと、IME のサイズに合わせて IME インセットがアニメーション化され、IME が開くと、imePadding() 修飾子によって下部パディングの適用が開始され、LazyColumn のサイズが変更されます。imePadding() 修飾子が下部パディングの適用を開始すると、その量のインセットの使用も開始します。したがって、システムバーの間隔の一部が imePadding() 修飾子によってすでに適用されているため、Spacer の高さが減少し始めます。imePadding() 修飾子が適用する下部パディングの量がシステムバーよりも大きいと、Spacer の高さはゼロになります。

IME が閉じると、変更が逆方向に行われます。imePadding() がシステムバーの下側より小さくなると、Spacer は高さ 0 から拡張し始め、IME が完全にアニメーション表示されると最終的に Spacer はシステムバーの下側の高さと一致します。

図 2.TextField を使用したエッジ ツー エッジの遅延列

この動作は、すべての windowInsetsPadding 修飾子間の通信を通じて実現され、いくつかの方法で影響を受けます。

Modifier.consumeWindowInsets(insets: WindowInsets)Modifier.windowInsetsPadding と同じ方法でインセットを使用しますが、使用されたインセットをパディングとして適用しません。これは、インセットのサイズ修飾子と組み合わせて使用すると、一定数のインセットがすでに消費されていることを兄弟に通知するために役立ちます。

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues)WindowInsets 引数が指定されたバージョンとよく似ていますが、任意の PaddingValues を受け取ります。これは、インセットのパディング修飾子以外のメカニズム(通常の Modifier.padding や固定の高さのスペーサーなど)によってパディングまたはスペースが設定されている場合に、子に通知するのに役立ちます。

@OptIn(ExperimentalLayoutApi::class)
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

未加工のウィンドウ インセットが消費なしで必要な場合は、WindowInsets 値を直接使用するか、WindowInsets.asPaddingValues() を使用して、消費の影響を受けないインセットの PaddingValues を返します。ただし、以下の注意点により、可能な限り、ウィンドウ インセットのパディング修飾子とウィンドウ インセットのサイズ修飾子を使用することをおすすめします。

インセットと Jetpack Compose のフェーズ

Compose は、基盤となる AndroidX コア API を使用して、インセットの更新とアニメーション化を行います。インセットを管理するプラットフォーム API を使用します。このようなプラットフォームの動作により、インセットは Jetpack Compose のフェーズと特別な関係にあります。

インセットの値は、コンポジション フェーズの後、レイアウト フェーズの前に更新されます。つまり、コンポジションのインセットの値を読み取る場合、通常は 1 フレーム遅れたインセットの値が使用されます。このページで説明する組み込み修飾子は、レイアウト フェーズまでインセットの値を使用して遅延するように構築されています。これにより、インセット値が更新時に同じフレームで使用されることが保証されます。

WindowInsets を使用したキーボード IME アニメーション

Modifier.imeNestedScroll() をスクロール コンテナに適用すると、コンテナの一番下までスクロールしたときに IME を自動的に開閉できます。

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

キーボードを表示するように UI 要素が上下方向にスクロールするアニメーション

図 1. IME アニメーション

マテリアル 3 コンポーネントのインセット サポート

使いやすさを考慮して、組み込みのマテリアル 3 コンポーザブル(androidx.compose.material3)の多くは、マテリアルの仕様に従ってアプリ内でのコンポーザブルの配置方法に基づいて、インセットを自身で処理します。

インセット処理コンポーザブル

インセットを自動的に処理するマテリアル コンポーネントのリストを以下に示します。

アプリバー

コンテンツ コンテナ

Scaffold

デフォルトでは、Scaffold はパラメータ paddingValues としてインセットを提供し、ユーザーがこれを使用できるようにします。Scaffold は、コンテンツにインセットを適用しません。この責任はユーザーにあります。たとえば、Scaffold 内の LazyColumn でこれらのインセットを使用するには、次のようにします。

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

デフォルトのインセットをオーバーライドする

コンポーザブルに渡される windowInsets パラメータを変更して、コンポーザブルの動作を構成できます。このパラメータは、代わりに適用する別のタイプのウィンドウ インセットにすることも、空のインスタンス WindowInsets(0, 0, 0, 0) を渡して無効にすることもできます。

たとえば、LargeTopAppBar のインセット処理を無効にするには、windowInsets パラメータを空のインスタンスに設定します。

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

ビューシステム インセットとの相互運用

同じ階層内に View と Compose コードの両方がある場合は、デフォルトのインセットをオーバーライドする必要があります。この場合、どちらがインセットを使用し、どちらがインセットを無視するかを明示的に指定する必要があります。

たとえば、最も外側のレイアウトが Android View レイアウトの場合、View システムでインセットを使用し、Compose では無視する必要があります。または、最も外側のレイアウトがコンポーザブルの場合は、Compose でインセットを使用し、それに応じて AndroidView コンポーザブルをパディングする必要があります。

デフォルトでは、各 ComposeView は消費レベル WindowInsetsCompat ですべてのインセットを使用します。このデフォルト動作を変更するには、ComposeView.consumeWindowInsetsfalse に設定します。

リソース

  • Now in Android - Kotlin と Jetpack Compose だけで構築された、完全に機能する Android アプリです。