Jetpack Composeでエッジツーエッジの没入感があるスクロールUIを実現する
Jetpack Compose を利用して、システム UI の領域を超えてエッジツーエッジでスクロール UI を描画し、没入感を演出する方法について書きます。
| 向き | これを(Before) | こうしたい(After) |
|---|---|---|
| 縦 | ![]() |
![]() |
| 横 | ![]() |
![]() |
公式ドキュメントなどでドンピシャなサンプルコードや説明がなく少し手間取ったので、メモしておきます。
結論
Scaffold の子 Compose に渡される PaddingValues には画面下部のナビゲーションバーの高さが含まれています。
これを LazyColumn の contentPadding にスクロール内部の余白として指定します。
また、WindowInsets.safeDrawing にはナビゲーションバー以外のシステム UI や切り欠きのサイズが含まれています。
これを、TopAppBar や LazyColumn の windowInsetsPadding に左右のサイズだけを取り出して指定します。
上記を行うことで、端末の画面いっぱいまでコンテンツが描画されつつ、視認性や操作感を損なわない UI が実現できます。
やりたいことの補足説明
前提として、やりたいことをもう少し具体的に説明すると、以下のような感じです。
- システム UI(ナビゲーションバー) の下にスクロールビューのコンテンツが描画され、一番下までスクロールした際に最後のアイテムがシステム UI に重ならない
- 左右にシステム UI や切り欠きがあった場合、描画が重ならない




コードを用いた詳細な解説
Before のコード
元々以下のようなコードを書いていました。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
MyScaffold()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffold() {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(
text = "Edge to edge",
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items((1..14).toList()) { index ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$index",
modifier = Modifier.padding(16.dp),
fontSize = 18.sp,
)
}
}
}
}
}
上記を実行すると、やりたいことの Before の状態となります。
- システム UI(ナビゲーションバー) の領域にスクロールビューのコンテンツが描画されていない
- 左右にシステム UI や切り欠きがあった場合、描画が重なってしまう
After のコードへの修正
1. LazyColumn の contentPadding を利用してシステム UI 分の余白を設定する
前提として、Scaffold の内部に渡される innerPadding の bottom は、システム UI(ナビゲーションバー)の高さ分の余白を含みます。
そのため、この値をスクロールビューの余白にうまく適用してやることで、やりたいことが実現できます。

Before のコードのように LazyColumn の modifier で余白を設定した場合、スクロールコンテンツが描画される領域(ビューポート)の外側に余白が適用されます。
そのため、スクロールコンテンツの描画領域がシステム UI に重ならないような状態となっていました。

レイヤー構造としては、以下のようなイメージです。

一方で、LazyColumn の contentPadding を利用すると、スクロールコンテンツの中身に余白が適用されます。
これを利用することで、スクロールコンテンツの中身にシステム UI の高さ分だけ余白を設定できます。

レイヤー構造としては、以下のようなイメージです。

これらを踏まえると、以下の方針とすることで、やりたいことが実現できます。
-
LazyColumnのmodifierにおける余白設定をしない。これにより、システム UI の領域にもスクロールコンテンツの描画領域が広がる。 -
LazyColumnのcontentPaddingを利用してシステム UI の高さ分だけ余白を設定する。これにより、スクロールコンテンツの中身がシステム UI に重ならないところまでスクロールできるようになる。
LazyColumn(
modifier = Modifier
.fillMaxSize()
- .padding(innerPadding),
// ...
- contentPadding = PaddingValues(16.dp),
+ contentPadding = PaddingValues(
+ start = 16.dp + innerPadding.calculateStartPadding(
+ LocalLayoutDirection.current
+ ),
+ top = 16.dp + innerPadding.calculateTopPadding(),
+ end = 16.dp + innerPadding.calculateEndPadding(
+ LocalLayoutDirection.current
+ ),
+ bottom = 16.dp + innerPadding.calculateBottomPadding(),
+ ),
2. WindowInsets.safeDrawing により切り欠きを避けて描画する
WindowInsets.safeDrawing には、ノッチやパンチホールなどの「切り欠き」領域を避けて描画するための情報が含まれています。

これを利用し、上下左右のうち必要な要素だけを取り出して、Modifier.windowInsetsPadding に渡すことで、切り欠きを避けて描画できます。
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
+ modifier = Modifier
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(WindowInsetsSides.Start)
+ )
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(WindowInsetsSides.End)
+ ),
// ...
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
// ...
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(WindowInsetsSides.Start)
+ )
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(WindowInsetsSides.End)
+ ),
// ...
完全なコード
最終的に以下のようなコードになりました。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
MyScaffold()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffold() {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
modifier = Modifier
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Start)
)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.End)
),
title = {
Text(
text = "Edge to edge",
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Start)
)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.End)
),
contentPadding = PaddingValues(
start = 16.dp + innerPadding.calculateStartPadding(
LocalLayoutDirection.current
),
top = 16.dp + innerPadding.calculateTopPadding(),
end = 16.dp + innerPadding.calculateStartPadding(
LocalLayoutDirection.current
),
bottom = 16.dp + innerPadding.calculateBottomPadding(),
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items((1..14).toList()) { index ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$index",
modifier = Modifier.padding(16.dp),
fontSize = 18.sp,
)
}
}
}
}
}
参考




Discussion