前提条件
- プロジェクトは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つの方法のみを選択してください。
- 推奨: 利用可能な場合は
Scaffoldを使用し、PaddingValuesをcontentラムダに渡します。
Scaffold { innerPadding -> // innerPadding はシステムバーとScaffoldコンポーネントの両方を考慮します LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* コンテンツ */ } }-
推奨: 利用可能な場合は、マテリアルコンポーネントの自動インセット処理またはパディング修飾子を使用します。
- Material 3コンポーネントは以下のコンポーネントのセーフエリアを自動管理します:
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- Material 2コンポーネントの場合は、
windowInsetsパラメータを使用してBottomAppBar、TopAppBar、BottomNavigationに手動でインセットを適用します。親コンテナにパディングを適用しないでください。代わりに、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("ログイン") } } -
深くネストされたコンポーネントでパディングが過剰な場合は、
WindowInsetsRulers(例:Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))を使用します。コードサンプルについてはIMEセクションを参照してください。 -
カスタムヘッダーや装飾的なスクリムなどの要素をシステムバーと同じ寸法にする必要がある場合は、インセットサイズ修飾子(例:
Modifier.windowInsetsTopHeight(WindowInsets.systemBars))を使用します。コードサンプルについてはリストセクションを参照してください。
- 推奨: 利用可能な場合は
アダプティブScaffold
NavigationSuiteScaffoldは、NavigationRailやNavigationBarなどの自身のコンポーネントのセーフエリアを管理します。ただし、アダプティブScaffold(例:NavigationSuiteScaffold、ListDetailPaneScaffold)はPaddingValuesを内部のコンテンツに伝播しません。ステップ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としてcontentラムダに渡されるため正しいです。
// 正しい
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* コンテンツ */ }
}
fitInsideがcontentWindowInsetsに関係なくIMEインセットにコンテンツを適合させるため正しいです。
// 正しい
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* コンテンツ */ }
}
デフォルトのcontentWindowInsetsにはIMEインセットが含まれておらず、imePadding()がIMEインセットを適用するため正しいです:
// 正しい
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* コンテンツ */ }
}
誤り
IMEが開いたときに余分なパディングが発生するため誤りです。IMEインセットが2回適用されます。1回目は渡されたcontentWindowInsetsに含まれるIMEインセットを持つinnerPadding、2回目はimePaddingです:
// 誤り
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* コンテンツ */ }
}
IMEがコンテンツを覆い隠すため誤りです。ScaffoldのデフォルトのcontentWindowInsetsにはIMEインセットが含まれていません。
// 誤り
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* コンテンツ */ }
}
Scaffoldを使用しないIMEのコードパターン
正しい
以下のコードサンプルでは過剰なパディングは発生しません。
// 正しい
Box(
// インセット消費済み
modifier = Modifier.safeDrawingPadding() // または imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* コンテンツ */ }
}
// 正しい
Box(
// インセット消費済み
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // または WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* コンテンツ */ }
}
// 正しい
Box(
// インセットは消費されないが、fitInsideにより問題なし
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // または WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* コンテンツ */ }
}
誤り
以下のコードサンプルではIMEインセットが2回適用されるため過剰なパディングが発生します:
// 誤り
Box(
// インセットは消費されない
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // または WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* コンテンツ */ }
}
ナビゲーションバーのコントラストとシステムバーアイコン
-
Activityが
WindowCompatのenableEdgeToEdgeを使用する場合、ライトテーマとダークテーマをサポートするアプリでは、システムバーアイコンを視認可能にするためにisAppearanceLightNavigationBarsとisAppearanceLightStatusBarsをデバイステーマの逆に設定する必要があります。テーマファイルでこれを行うことを推奨します。ActivityがComponentActivityのenableEdgeToEdgeを使用する場合はこれを行わないでください。アイコンの色を自動的に処理するためです。// `WindowCompat`の`enableEdgeToEdge`を呼び出す場合のみ使用します。 // テーマファイルに適用してください。 @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) // ライトモードではダークアイコン、ダークモードではライトアイコン controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) } -
画面が
ScaffoldまたはNavigationSuiteScaffoldとボトムバー(例:BottomAppBar、NavigationBar)を使用する場合、対応するActivityでwindow.isNavigationBarContrastEnforced = falseをSDK 29+に設定します。これにより、システムがナビゲーションバーに半透明の背景を追加するのを防ぎ、ボトムバーの色が画面の下部まで拡張されることを確認します。
リスト
- インセットパディング(
ScaffoldのinnerPaddingなど)をスクロール可能なコンポーネント(例:LazyColumn、LazyRow)のcontentPaddingパラメータに適用します。リストの親コンテナにModifier.padding()として適用しないでください。コンテンツをクリッピングし、システムバーの背後でスクロールできなくなるためです。 - システムバーを覆う半透明のコンポーザブルを作成して、アイコンが視認可能な状態を保ちます。
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge は window.isNavigationBarContrastEnforced = true を設定します
// これは3ボタンナビゲーションに半透明のスクリムを追加するために使用されます
enableEdgeToEdge()
setContent {
MyTheme {
// メインコンテンツ
MyContent()
// メインコンテンツの描画後、ステータスバーの保護を描画
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
)
)
)
)
}
ダイアログ
以下の2つの条件が両方とも真の場合、ダイアログは全画面となり、エッジ トゥ エッジ対応にする必要があります:
DialogPropertiesにusePlatformDefaultWidth = falseが含まれている。- ダイアログが
Modifier.fillMaxSize()を呼び出している。
全画面ダイアログをエッジ トゥ エッジ対応にするには、DialogPropertiesでdecorFitsSystemWindows = falseを設定します。
Dialog(
onDismissRequest = { /* 却下を処理 */ },
properties = DialogProperties(
// 1. ダイアログが画面の全幅に広がることを許可
usePlatformDefaultWidth = false,
// 2. ダイアログがステータスバーとナビゲーションバーの背後に描画されることを許可
decorFitsSystemWindows = false
)
) { /* コンテンツ */ }
チェックリスト
- すべての
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を実行して確認してください。