ビューでコンテンツをエッジ ツー エッジで表示する

Compose をお試しください
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でエッジ ツー エッジを操作する方法を学びます。

Android 15 以降を搭載したデバイスで SDK 35 以降をターゲットにすると、アプリはエッジ ツー エッジで表示されます。ウィンドウは、システムバーの背後に描画することで、ディスプレイの幅と高さ全体に広がります。システムバーには、ステータスバー、キャプション バー、ナビゲーション バーが含まれます。

多くのアプリにはトップ アプリバーがあります。トップ アプリバーは画面の上端まで引き伸ばされ、ステータスバーの背後に表示されます。必要に応じて、コンテンツのスクロール時に上部アプリバーをステータスバーの高さまで縮小できます。

多くのアプリには、ボトム アプリバーまたは下部のナビゲーション バーもあります。これらのバーも画面の下端まで引き伸ばし、ナビゲーション バーの背後に表示する必要があります。それ以外の場合、アプリはナビゲーション バーの背後にスクロール コンテンツを表示する必要があります。

図 1. エッジ ツー エッジ レイアウトのシステムバー。

アプリにエッジ ツー エッジ レイアウトを実装する際は、次の点に注意してください。

  1. エッジ ツー エッジ表示を有効にする
  2. アダプティブ レイアウトを実装して、さまざまなフォーム ファクタでユーザー エクスペリエンスを最適化する
  3. 視覚的な重複を処理する
  4. システムバーの背後にスクリムを表示することを検討する
ステータスバーの背後に画像が表示されている例
図 2. ステータスバーの背後に画像が表示されている例。

エッジ ツー エッジ表示を有効にする

アプリが SDK 35 以降をターゲットとしている場合、Android 15 以降のデバイスではエッジツーエッジが自動的に有効になります。

以前の Android バージョンでエッジ ツー エッジを有効にするには、Activity の onCreate で enableEdgeToEdge を手動で呼び出します。

Kotlin

 override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         WindowCompat.enableEdgeToEdge(window)
        ...
      }

Java

 @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WindowCompat.enableEdgeToEdge(getWindow());
        ...
      }

デフォルトでは、enableEdgeToEdge() はシステムバーを透明にします。ただし、3 ボタン ナビゲーション モードでは、ナビゲーション バーに半透明のスクリムが適用されます。システム アイコンとスクリムの色は、システムのライトモードまたはダークモードに基づいて調整されます。

enableEdgeToEdge() 関数を使用せずにアプリでエッジ ツー エッジ表示を有効にするには、エッジ ツー エッジ表示を手動で設定するをご覧ください。

インセットを使用して重複を処理する

図 3 に示すように、アプリのビューの一部がシステムバーの背後に描画されることがあります。

重複に対処するには、インセットに反応します。インセットは、画面のどの部分がシステム UI(ナビゲーション バーやステータスバーなど)と交差するかを指定します。交差とは、コンテンツの上に表示されることを意味しますが、システム ジェスチャーについてアプリに通知することもできます。

アプリをエッジツーエッジで表示する際に適用されるインセットの種類は次のとおりです。

  • システムバー インセット: タップ可能で、システムバーによって視覚的に隠れてはならないビューに最適です。

  • ディスプレイ カットアウト インセット: デバイスの形状により、画面のカットアウトが発生する可能性がある領域。

  • システム ジェスチャー インセット: システムで使用されるジェスチャー ナビゲーション領域。アプリよりも優先されます。

システムバーのインセット

システムバー インセットは、最もよく使用されるインセット タイプです。これは、システム UI がアプリの上に Z 軸で表示される領域を表します。タップ可能で、システムバーによって視覚的に隠されてはならないアプリ内のビューを移動またはパディングするために使用するのが最適です。

たとえば、図 3 のフローティング アクション ボタン(FAB)は、ナビゲーション バーによって部分的に隠れています。

エッジ ツー エッジが実装されているが、ナビゲーション バーが FAB を覆っている例
図 3. エッジ ツー エッジ レイアウトで FAB と重なっているナビゲーション バー。

ジェスチャー モードまたはボタンモードでこのような視覚的な重なりを回避するには、WindowInsetsCompat.Type.systemBars() で getInsets(int) を使用してビューのマージンを増やします。

次のコード例は、システムバーのインセットを実装する方法を示しています。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(fab) { v, windowInsets ->
  val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
  // Apply the insets as a margin to the view. This solution sets
  // only the bottom, left, and right dimensions, but you can apply whichever
  // insets are appropriate to your layout. You can also update the view padding
  // if that's more appropriate.
  v.updateLayoutParams<MarginLayoutParams> {
      leftMargin = insets.left
      bottomMargin = insets.bottom
      rightMargin = insets.right
  }

  // Return CONSUMED if you don't want the window insets to keep passing
  // down to descendant views.
  WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(fab, (v, windowInsets) -> {
  Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
  // Apply the insets as a margin to the view. This solution sets only the
  // bottom, left, and right dimensions, but you can apply whichever insets are
  // appropriate to your layout. You can also update the view padding if that's
  // more appropriate.
  MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
  mlp.leftMargin = insets.left;
  mlp.bottomMargin = insets.bottom;
  mlp.rightMargin = insets.right;
  v.setLayoutParams(mlp);

  // Return CONSUMED if you don't want the window insets to keep passing
  // down to descendant views.
    return WindowInsetsCompat.CONSUMED;
});

このソリューションを図 3 の例に適用すると、図 4 に示すように、ボタンモードで視覚的な重複は発生しません。

FAB を覆わない半透明のナビゲーション バー
図 4. ボタンモードでの視覚的な重複を解決。

図 5 に示すように、ジェスチャー ナビゲーション モードでも同様です。

ジェスチャー ナビゲーションを使用したエッジ ツー エッジ
図 5. ジェスチャー ナビゲーション モードでの視覚的な重複を解決する。

ディスプレイ カットアウトのインセット

一部のデバイスにはディスプレイ カットアウトがあります。通常、カットアウトは画面の上部にあり、ステータスバーに含まれます。デバイスの画面が横向きの場合、カットアウトは縦方向の端に表示されることがあります。アプリが画面に表示するコンテンツに応じて、ディスプレイ カットアウトを避けるためにパディングを実装する必要があります。デフォルトでは、アプリはディスプレイ カットアウトに描画されます。

たとえば、多くのアプリ画面にはアイテムのリストが表示されます。ディスプレイ カットアウトやシステムバーでリストアイテムが隠れないようにします。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
  val bars = insets.getInsets(
    WindowInsetsCompat.Type.systemBars()
      or WindowInsetsCompat.Type.displayCutout()
  )
  v.updatePadding(
    left = bars.left,
    top = bars.top,
    right = bars.right,
    bottom = bars.bottom,
  )
  WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(mBinding.recyclerView, (v, insets) -> {
  Insets bars = insets.getInsets(
    WindowInsetsCompat.Type.systemBars()
    | WindowInsetsCompat.Type.displayCutout()
  );
  v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
  return WindowInsetsCompat.CONSUMED;
});

システムバーとディスプレイ カットアウト タイプの論理 OR を取得して、WindowInsetsCompat の値を決定します。

clipToPadding を RecyclerView に設定して、パディングがリストアイテムとともにスクロールされるようにします。これにより、次の例に示すように、ユーザーがスクロールしたときにアイテムがシステムバーの背後に移動します。

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

システム ジェスチャー インセット

システム ジェスチャー インセットは、システム ジェスチャーがアプリよりも優先されるウィンドウの領域を表します。これらの領域は、図 6 でオレンジ色で示されています。

システム ジェスチャー インセットの例
図 6. システム ジェスチャー インセット。

システムバーのインセットと同様に、WindowInsetsCompat.Type.systemGestures() で getInsets(int) を使用することで、システム ジェスチャーのインセットの重複を回避できます。

これらのインセットを使用して、スワイプ可能なビューを端から離すか、パディングします。一般的なユースケースとしては、ボトムシート、ゲームでのスワイプ、ViewPager2 を使用して実装されたカルーセルなどがあります。

Android 10 以降では、システム ジェスチャー インセットに、ホーム ジェスチャー用の下部インセットと、戻るジェスチャー用の左インセットおよび右インセットが含まれています。

システム ジェスチャー インセットの測定例
図 7. システム ジェスチャー インセットの測定。

次のコード例は、システム ジェスチャー インセットを実装する方法を示しています。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
    val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
    // Apply the insets as padding to the view. Here, set all the dimensions
    // as appropriate to your layout. You can also update the view's margin if
    // more appropriate.
    view.updatePadding(insets.left, insets.top, insets.right, insets.bottom)

    // Return CONSUMED if you don't want the window insets to keep passing down
    // to descendant views.
    WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> {
    Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
    // Apply the insets as padding to the view. Here, set all the dimensions
    // as appropriate to your layout. You can also update the view's margin if
    // more appropriate.
    view.setPadding(insets.left, insets.top, insets.right, insets.bottom);

    // Return CONSUMED if you don't want the window insets to keep passing down
    // to descendant views.
    return WindowInsetsCompat.CONSUMED;
});

マテリアル コンポーネント

ビューベースの Android マテリアル コンポーネント(com.google.android.material)の多くは、インセットを自動的に処理します。たとえば、BottomAppBar、BottomNavigationView、NavigationRailView、NavigationView などです。

ただし、AppBarLayout はインセットを自動的に処理しません。android:fitsSystemWindows="true" を追加して、上部のインセットを処理します。

Compose のマテリアル コンポーネントでインセットを処理する方法を確認する。

下位互換性のあるインセット ディスパッチ

インセットが子ビューにディスパッチされないようにして、過剰なパディングを回避するには、WindowInsetsCompat.CONSUMED 定数を使用してインセットを使用します。ただし、Android 10(API レベル 29 以前)を搭載したデバイスでは、WindowInsetsCompat.CONSUMED を呼び出した後にインセットが兄弟にディスパッチされないため、意図しない視覚的な重複が発生する可能性があります。

インセット ディスパッチが機能しない例
図 8. インセットのディスパッチが壊れている例。 Android 10(API レベル 29)以前では、ViewGroup 1 がインセットを消費した後にインセットが兄弟ビューにディスパッチされないため、TextView 2 がシステム ナビゲーション バーと重なります。ただし、Android 11(API レベル 30)以降では、インセットは想定どおりに兄弟ビューにディスパッチされます。

サポートされているすべての Android バージョンでインセットが兄弟にディスパッチされることを確認するには、インセットを使用する前に ViewGroupCompat#installCompatInsetsDispatch を使用します。これは、AndroidX Core と Core-ktx 1.16.0-alpha01 以降で使用できます。

Kotlin

// Use the i.d. assigned to your layout's root view, e.g. R.id.main
val rootView = findViewById(R.id.main)
// Call before consuming insets
ViewGroupCompat.installCompatInsetsDispatch(rootView)

Java

// Use the i.d. assigned to your layout's root view, e.g. R.id.main
LinearLayout rootView = findViewById(R.id.main);
// Call before consuming insets
ViewGroupCompat.installCompatInsetsDispatch(rootView);
インセット ディスパッチの例を修正
図 9. ViewGroupCompat#installCompatInsetsDispatch の呼び出し後のインセット ディスパッチを修正しました。

没入モード

コンテンツによっては、全画面表示にすることでユーザー エクスペリエンスが向上し、没入感が高まる場合があります。没入モードのシステムバーを非表示にするには、WindowInsetsController ライブラリと WindowInsetsControllerCompat ライブラリを使用します。

Kotlin

val windowInsetsController =
      WindowCompat.getInsetsController(window, window.decorView)

// Hide the system bars.
windowInsetsController.hide(Type.systemBars())

// Show the system bars.
windowInsetsController.show(Type.systemBars())

Java

Window window = getWindow();
WindowInsetsControllerCompat windowInsetsController =
      WindowCompat.getInsetsController(window, window.getDecorView());
if (windowInsetsController == null) {
    return;
  }
// Hide the system bars.
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());

// Show the system bars.
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());

この機能の実装について詳しくは、没入モードのシステムバーを非表示にするをご覧ください。

システムバー アイコン

enableEdgeToEdge を呼び出すと、デバイスのテーマが変更されたときにシステムバーのアイコンの色が更新されます。

エッジ ツー エッジにする場合、システムバーのアイコンの色を手動で更新して、アプリの背景とコントラストを付ける必要がある場合があります。たとえば、明るいステータスバー アイコンを作成するには、次のようにします。

Kotlin

WindowCompat.getInsetsController(window, window.decorView)
    .isAppearanceLightStatusBars = false

Java

WindowCompat.getInsetsController(window, window.getDecorView())
    .setAppearanceLightStatusBars(false);

システムバーの保護

アプリが SDK 35 以降をターゲットにすると、エッジ ツー エッジが適用されます。システム ステータスバーとジェスチャー ナビゲーション バーは透明ですが、3 ボタン ナビゲーション バーは半透明です。enableEdgeToEdge を呼び出して、下位互換性を確保します。

ただし、システムのデフォルトがすべてのユースケースで機能するとは限りません。透明または半透明のシステムバーを使用するかどうかを判断するには、Android システムバーの設計ガイダンスとエッジ ツー エッジ設計をご覧ください。

透明なシステムバーを作成する

Android 15(SDK 35)以降をターゲットにするか、以前のバージョンのデフォルト引数で enableEdgeToEdge() を呼び出して、透明なステータスバーを作成します。

Android 15 以降をターゲットにするか、以前のバージョンでデフォルトの引数を指定して enableEdgeToEdge() を呼び出すことで、透明なジェスチャー ナビゲーション バーを作成します。3 ボタン ナビゲーション バーの場合は、Window.setNavigationBarContrastEnforced を false に設定します。そうしないと、半透明のスクリムが適用されます。

半透明のシステムバーを作成する

半透明のステータスバーを作成する手順は次のとおりです。

  1. androidx-core 依存関係を 1.16.0-beta01 以降に更新します。
  2. XML レイアウトを androidx.core.view.insets.ProtectionLayout でラップし、ID を割り当てます。
  3. プログラマティックに ProtectionLayout にアクセスして、ステータスバーの側面と GradientProtection を指定して保護を設定します。

<androidx.core.view.insets.ProtectionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_protection"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:id="@+id/item_list"
        android:clipToPadding="false"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--items-->

    </ScrollView>

</androidx.core.view.insets.ProtectionLayout>

findViewById<ProtectionLayout>(R.id.list_protection)
    .setProtections(
        listOf(
            GradientProtection(
                WindowInsetsCompat.Side.TOP,
                // Ideally, this is the pane's background color
                paneBackgroundColor
            )
        )
    )

GradientProtection に渡される ColorInt がコンテンツの背景と一致していることを確認します。たとえば、折りたたみ式デバイスに表示されるリストと詳細のレイアウトでは、リストパネルと詳細パネルに異なる色の異なる GradientProtections が設定されている場合があります。

図 1. さまざまな色のグラデーション保護。

半透明のジェスチャー ナビゲーション バーは作成しないでください。半透明の 3 ボタン ナビゲーション バーを作成するには、次のいずれかを行います。

  • レイアウトがすでに ProtectionView でラップされている場合は、setProtections メソッドに ColorProtection または GradientProtection を追加で渡すことができます。その前に、window.isNavigationBarContrastEnforced = false を確認してください。
  • それ以外の場合は、window.isNavigationBarContrastEnforced = true を設定します。

その他のヒント

インセットを処理する際のその他のヒント。

スクロール コンテンツを端から端まで表示する

インセットを処理し、clipToPadding を false に設定することで、RecyclerView または NestedScrollView のシステムバーによってリストの最後のアイテムが隠れないようにします。

次の動画は、エッジ ツー エッジ表示が無効(左)と有効(右)の RecyclerView を示しています。

コードサンプルについては、RecyclerView で動的リストを作成するセクションのコード スニペットをご覧ください。

全画面表示ダイアログをエッジ ツー エッジにする

全画面表示ダイアログをエッジ ツー エッジにするには、ダイアログで enableEdgeToEdge を呼び出します。

Kotlin

class MyAlertDialogFragment : DialogFragment() {
    override fun onStart(){
        super.onStart()
        dialog?.window?.let { WindowCompat.enableEdgeToEdge(it) }
    }
    ...
}

Java

public class MyAlertDialogFragment extends DialogFragment {
    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        if (dialog != null) {
            Window window = dialog.getWindow();
            if (window != null) {
                WindowCompat.enableEdgeToEdge(window);
            }
        }
    }
    ...
}

参考情報

エッジ ツー エッジの実装について詳しくは、以下のリファレンスをご覧ください。

ブログ

デザイン

その他のドキュメント

動画