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 つの「安全な」インセット型がまとめられています。

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

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

インセットのセットアップ

アプリがコンテンツを描画する場所を完全に制御できるようにするには、次のセットアップ手順を実施します。この手順を行わないと、システム 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() があり、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 のサイズに合わせてアニメーション表示され、imePadding() 修飾子が下部パディングの適用を開始し、IME が開くと 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 コンポーザブルをパディングする必要があります。

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

リソース

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