edge-to-edge について
Android Jetpack Compose プロジェクト向けに、Activity、リスト、入力画面のエッジツーエッジ対応と、システムバーや IME の Insets 処理を案内するスキルです。target SDK 35 以上が必要です。
主な機能
- 01Activity、リスト、FABの対応状況を確認
- 02enableEdgeToEdgeとadjustResizeを設定
- 03ScaffoldとシステムInsetsでレイアウト管理
- 04IMEの重なりと二重パディングに対処
- 05ダイアログとシステムバーアイコンの明暗に対応
活用シーン
- 01Composeアプリにエッジツーエッジ対応を追加
- 02キーボードによる入力欄の隠れを修正
- 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:計画
- すべての Activity クラスを特定して分析し、既存のエッジツーエッジ対応の有無を確認します。エッジツーエッジに対応していない Activity はすべて、エッジツーエッジにする計画を立てます。
- 各 Activity で、すべてのリストと FAB コンポーネントを特定して分析し、既存のエッジツーエッジ対応の有無を確認します。エッジツーエッジに対応していないコンポーネントはすべて、エッジツーエッジにする計画を立てます。
- 各 Activity で、
TextField、OutlinedTextField、BasicTextFieldを検索します。見つかった場合は、このスキルの IME セクションに従い、IME によって入力フィールドが隠れないことを必ず確認します。
ステップ 2:エッジツーエッジ対応を追加する
- まだ
enableEdgeToEdgeを呼び出していない Activity では、それぞれのonCreate内でsetContentの前にenableEdgeToEdgeを追加します。 - ソフトキーボードを使用するすべての Activity について、AndroidManifest.xml に
android:windowSoftInputMode="adjustResize"を追加します。
ステップ 3:インセットを適用する
-
重要な UI をタップできる状態に保つため、アプリではシステムインセットを適用するか、コンテンツをルーラーに揃える必要があります。二重のパディングを避けるため、次の方法から 1 つだけ選択してください。
- 推奨: 利用可能な場合は
Scaffolds を使用し、PaddingValuesをコンテンツラムダに渡します。
Scaffold { innerPadding -> // innerPadding accounts for system bars and any Scaffold components LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* Content */ } }
-
推奨: 利用可能な場合は、Material コンポーネントの自動インセット処理またはパディング修飾子を使用します。
- Material 3 コンポーネントは、次のものを含め、それぞれのコンポーネントのセーフエリアを管理します。
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- Material 2 コンポーネントでは、
BottomAppBar、TopAppBar、BottomNavigationに手動でインセットを適用するためにwindowInsetsparameter を使用します。親コンテナにはパディングを適用しないでください。代わりに、インセットを App Bar コンポーネントに直接渡します。親コンテナにパディングを適用すると、App Bar の背景がシステムバー領域まで描画されなくなります。たとえば、TopAppBarでは次のいずれか 1 つだけを選択します。- 推奨:
TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets) TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
- 推奨:
- Material 3 コンポーネントは、次のものを含め、それぞれのコンポーネントのセーフエリアを管理します。
-
Scaffold の外部にあるコンポーネントでは、
Modifier.safeDrawingPadding()やModifier.windowInsetsPadding(WindowInsets.safeDrawing)などのパディング修飾子を使用します。Box( modifier = Modifier .fillMaxSize() .safeDrawingPadding() ) { Button( onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("Login") } }
-
深くネストされたコンポーネントでパディングが過剰な場合は、
WindowInsetsRulers(例:Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))を使用します。コード例については、IME セクションを参照してください。 -
要素(カスタムヘッダーや装飾用スクリーンなど)をシステムバーと同じ寸法にする必要がある場合は、インセットサイズ修飾子(例:
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は使用しないでください。その後、入力フィールドにフォーカスを維持します。次のいずれかを選択してください。-
- 推奨: コンテンツコンテナーに
Modifier.fitInside(WindowInsetsRulers.Ime.current)を追加します。階層内の上流でインセットを消費し忘れた場合に発生するジャンクや余分なパディングを減らせるため、imePadding()よりもこちらが推奨されます。
- 推奨: コンテンツコンテナーに
-
- コンテンツコンテナーに
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
)
)
)
)
}
## ダイアログ
次の両方の条件を満たす場合、ダイアログはフルスクリーンとなり、画面端まで表示する必要があります。
DialogPropertiesにusePlatformDefaultWidth = falseが含まれている。- ダイアログで
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を実行してください。