mcpField
E
edge-to-edge

edge-to-edge

github.com/androidデザイン・UI10.0k3.3k 回インストール2 日前に更新
リポジトリ

edge-to-edge について

Android Jetpack Compose プロジェクト向けに、Activity、リスト、入力画面のエッジツーエッジ対応と、システムバーや IME の Insets 処理を案内するスキルです。target SDK 35 以上が必要です。

主な機能

  1. 01Activity、リスト、FABの対応状況を確認
  2. 02enableEdgeToEdgeとadjustResizeを設定
  3. 03ScaffoldとシステムInsetsでレイアウト管理
  4. 04IMEの重なりと二重パディングに対処
  5. 05ダイアログとシステムバーアイコンの明暗に対応

活用シーン

  1. 01Composeアプリにエッジツーエッジ対応を追加
  2. 02キーボードによる入力欄の隠れを修正
  3. 03リストやFABをシステムバーから離して配置

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/android/skills --skill edge-to-edge

インストール後、Claude Code で/edge-to-edgeを入力して読み込まれたか確認します。

詳細

前提条件

  • プロジェクトは Android Jetpack Compose を使用する必要があります。
  • プロジェクトのターゲット SDK は 35 以上である必要があります。SDK が 35 未満の場合は、SDK を 35 に引き上げてください。

ステップ 1:計画

  1. すべての Activity クラスを特定して分析し、既存のエッジツーエッジ対応の有無を確認します。エッジツーエッジに対応していない Activity はすべて、エッジツーエッジにする計画を立てます。
  2. 各 Activity で、すべてのリストと FAB コンポーネントを特定して分析し、既存のエッジツーエッジ対応の有無を確認します。エッジツーエッジに対応していないコンポーネントはすべて、エッジツーエッジにする計画を立てます。
  3. 各 Activity で、TextField、OutlinedTextField、BasicTextField を検索します。見つかった場合は、このスキルの IME セクションに従い、IME によって入力フィールドが隠れないことを必ず確認します。

ステップ 2:エッジツーエッジ対応を追加する

  1. まだ enableEdgeToEdge を呼び出していない Activity では、それぞれの onCreate 内で setContent の前に enableEdgeToEdge を追加します。
  2. ソフトキーボードを使用するすべての Activity について、AndroidManifest.xml に android:windowSoftInputMode="adjustResize" を追加します。

ステップ 3:インセットを適用する

  • 重要な UI をタップできる状態に保つため、アプリではシステムインセットを適用するか、コンテンツをルーラーに揃える必要があります。二重のパディングを避けるため、次の方法から 1 つだけ選択してください。

    1. 推奨: 利用可能な場合は Scaffolds を使用し、PaddingValues をコンテンツラムダに渡します。
    Scaffold { innerPadding ->
        // innerPadding accounts for system bars and any Scaffold components
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .consumeWindowInsets(innerPadding),
            contentPadding = innerPadding
        ) { /* Content */ }
    }
    

    1. 推奨: 利用可能な場合は、Material コンポーネントの自動インセット処理またはパディング修飾子を使用します。

      • Material 3 コンポーネントは、次のものを含め、それぞれのコンポーネントのセーフエリアを管理します。
        • TopAppBar
        • SmallTopAppBar
        • CenterAlignedTopAppBar
        • MediumTopAppBar
        • LargeTopAppBar
        • BottomAppBar
        • ModalDrawerSheet
        • DismissibleDrawerSheet
        • PermanentDrawerSheet
        • ModalBottomSheet
        • NavigationBar
        • NavigationRail
      • Material 2 コンポーネントでは、BottomAppBar、TopAppBar、BottomNavigation に手動でインセットを適用するために windowInsetsparameter を使用します。親コンテナにはパディングを適用しないでください。代わりに、インセットを App Bar コンポーネントに直接渡します。親コンテナにパディングを適用すると、App Bar の背景がシステムバー領域まで描画されなくなります。たとえば、TopAppBar では次のいずれか 1 つだけを選択します。
        1. 推奨: TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)
        2. TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))
        3. TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
    2. Scaffold の外部にあるコンポーネントでは、Modifier.safeDrawingPadding() や Modifier.windowInsetsPadding(WindowInsets.safeDrawing) などのパディング修飾子を使用します。

      Box(
          modifier = Modifier
              .fillMaxSize()
              .safeDrawingPadding()
      ) {
          Button(
              onClick = {},
              modifier = Modifier.align(Alignment.BottomCenter)
          ) {
              Text("Login")
          }
      }
      

    3. 深くネストされたコンポーネントでパディングが過剰な場合は、WindowInsetsRulers(例:Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))を使用します。コード例については、IME セクションを参照してください。

    4. 要素(カスタムヘッダーや装飾用スクリーンなど)をシステムバーと同じ寸法にする必要がある場合は、インセットサイズ修飾子(例:Modifier.windowInsetsTopHeight(WindowInsets.systemBars))を使用します。 コード例については、Lists セクションを参照してください。

アダプティブスキャフォールド

  • NavigationSuiteScaffold は、NavigationRail や NavigationBar などの自身のコンポーネントのセーフエリアを管理します。ただし、アダプティブスキャフォールド(例:NavigationSuiteScaffold、ListDetailPaneScaffold)は、内側のコンテンツに PaddingValues を伝播しません。Step 3 の説明に従って、個々の画面またはコンポーネント(例:リストの contentPadding や FAB のパディング)にインセットを適用する必要があります。NavigationSuiteScaffold の親に safeDrawingPadding や同様の修飾子を適用しないでください。これを行うと、画面がクリップされ、エッジツーエッジ表示が妨げられます。

IME

  • ソフトキーボードを使用する各 Activity で、AndroidManifest.xml に android:windowSoftInputMode="adjustResize" が設定されていることを確認してください。非推奨の SOFT_INPUT_ADJUST_RESIZE は使用しないでください。その後、入力フィールドにフォーカスを維持します。次のいずれかを選択してください。
      1. 推奨: コンテンツコンテナーに Modifier.fitInside(WindowInsetsRulers.Ime.current) を追加します。階層内の上流でインセットを消費し忘れた場合に発生するジャンクや余分なパディングを減らせるため、imePadding() よりもこちらが推奨されます。
      1. コンテンツコンテナーに imePadding を追加します。パディング修飾子は Modifier.verticalScroll() より前に配置する必要があります。親がすでに contentWindowInsets(例:contentWindowInsets = WindowInsets.safeDrawing)で IME を考慮している場合は、Modifier.imePadding() を使用しないでください。二重にパディングされます。
Scaffold を使った IME のコードパターン
正しい例

contentWindowInsets に IME インセットが含まれており、コンテンツのラムダに innerPadding として渡されるため、正しい例です。

// RIGHT
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* Content */ }
}


contentWindowInsets の内容にかかわらず、fitInside がコンテンツを IME インセットに合わせるため、正しい例です。

// RIGHT
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .fitInside(WindowInsetsRulers.Ime.current)
            .verticalScroll(rememberScrollState())
    ) { /* Content */ }
}


デフォルトの contentWindowInsets に IME インセットが含まれておらず、imePadding() が IME インセットを適用するため、正しい例です。

// RIGHT
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* Content */ }
}

誤った例

IME が開いたときに余分なパディングが発生するため、誤った例です。渡された contentWindowInsets の値に含まれる IME インセットが innerPadding によって適用され、さらに imePadding によっても適用されるため、IME インセットが二重に適用されます。

// WRONG
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* Content */ }
}


Scaffold のデフォルトの contentWindowInsets には IME インセットが含まれていないため、IME がコンテンツに覆いかぶさる誤った例です。

// WRONG
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* Content */ }
}

Scaffold を使わない IME のコードパターン
正しい例

次のコードサンプルでは、過剰なパディングは発生しません。

// RIGHT
Box(
    // Insets consumed
    modifier = Modifier.safeDrawingPadding() // or imePadding(), safeContentPadding(), safeGesturesPadding()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Content */ }
}


// RIGHT
Box(
    // Insets consumed
    modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Content */ }
}


// RIGHT
Box(
    // Insets not consumed, but irrelevant due to fitInside
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .fitInside(WindowInsetsRulers.Ime.current)
    ) { /* Content */ }
}

誤った例

次のコードサンプルでは、IME インセットが二重に適用されるため、過剰なパディングが発生します。

// WRONG
Box(
    // Insets not consumed
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Content */ }
}

ナビゲーションバーのコントラストとシステムバーのアイコン

  • Activity が WindowCompat の enableEdgeToEdge を使用する場合、ライトテーマとダークテーマに対応するアプリでは、システムバーのアイコンが読みやすくなるように、isAppearanceLightNavigationBars と isAppearanceLightStatusBars をデバイスのテーマと反対の値に設定する必要があります。テーマファイルで設定することをおすすめします。Activity が ComponentActivity の enableEdgeToEdge を使用する場合は、アイコンの色が自動的に処理されるため、これは行わないでください。

    // Only use if calling `enableEdgeToEdge` from `WindowCompat`.
    // Apply to your theme file.
    @Composable
    fun MyTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
        val view = LocalView.current
        if (!view.isInEditMode) {
            SideEffect {
                val window = (view.context as? Activity)?.window ?: return@SideEffect
                val controller = WindowCompat.getInsetsController(window, view)
    
                // Dark icons for Light Mode (!darkTheme), Light icons for Dark Mode
                controller.isAppearanceLightStatusBars = !darkTheme
                controller.isAppearanceLightNavigationBars = !darkTheme
            }
        }
    
        MaterialTheme(content = content)
    }
    

  • 画面で下部バー(例:BottomAppBar、NavigationBar)を含む Scaffold または NavigationSuiteScaffold を使用する場合、SDK 29 以降では、該当する Activity で window.isNavigationBarContrastEnforced = false を設定してください。これにより、システムがナビゲーションバーに半透明の背景を追加しないようにし、下部バーの色が画面の最下部まで表示されることを確認できます。

リスト

  • Scaffold の innerPadding などのインセットパディングは、スクロール可能なコンポーネント(例:LazyColumn、LazyRow)の contentPadding パラメーターに適用してください。リストの親コンテナーに Modifier.padding() として適用しないでください。コンテンツがクリップされ、システムバーの背後までスクロールできなくなります。
  • システムバーを覆う半透明のコンポーザブルを作成し、アイコンが読みやすい状態を維持してください。
class SystemBarProtectionSnippets : ComponentActivity() {

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

        // enableEdgeToEdge sets window.isNavigationBarContrastEnforced = true
        // which is used to add a translucent scrim to three-button navigation
        enableEdgeToEdge()

        setContent {
            MyTheme {
                // Main content
                MyContent()

                // After drawing main content, draw status bar protection
                StatusBarProtection()
            }
        }
    }
}

@Composable
private fun StatusBarProtection(
    color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
    Spacer(
        modifier = Modifier
            .fillMaxWidth()
            .height(
                with(LocalDensity.current) {
                    (WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
                }
            )
            .background(
                brush = Brush.verticalGradient(
                    colors = listOf(
                        color.copy(alpha = 1f),
                        color.copy(alpha = 0.8f),
                        Color.Transparent
                    )
                )
            )
    )
}

## ダイアログ

次の両方の条件を満たす場合、ダイアログはフルスクリーンとなり、画面端まで表示する必要があります。

  1. DialogProperties に usePlatformDefaultWidth = false が含まれている。
  2. ダイアログで Modifier.fillMaxSize() を呼び出している。

フルスクリーンダイアログを画面端まで表示するには、DialogProperties で decorFitsSystemWindows = false を設定します。

Dialog(
    onDismissRequest = { /* Handle dismiss */ },
    properties = DialogProperties(
        // 1. Allows the dialog to span the full width of the screen
        usePlatformDefaultWidth = false,
        // 2. Allows the dialog to draw behind status and navigation bars
        decorFitsSystemWindows = false
    )
) { /* Content */ }

チェックリスト

  • [ ] すべての Activity で enableEdgeToEdge() を呼び出していますか?
  • [ ] AndroidManifest.xml で adjustResize を設定していますか?
  • [ ] すべての TextField、OutlinedTextField、または BasicTextField に、imePadding()、fitInside、Modifier.safeDrawingPadding()、Modifier.safeContentPadding()、Modifier.safeGesturesPadding()、または contentWindowInsets に WindowInsets.safeDrawing か WindowInsets.ime を設定した親がありますか?
  • [] リストの最初と最後の項目は、インセットを contentPadding に渡してシステムバーから離して表示されていますか?
  • [] FAB は Scaffold 内に配置するか、Modifier.safeDrawingPadding() を適用して、ナビゲーションバーの上に表示されていますか?
  • [] プロジェクトはビルドできますか?確認のため、./gradlew build を実行してください。

コメント