Android 15 の動作変更点の一つである Edge-to-edge enforcement を読みましたか? アプリの対象 API レベル 35 に上げると edge-to-edge が強制適用されるとのこと。これまで edge-to-edge を無視して来てしまい、既存アプリに edge-to-edge が適用されたらどうなってしまうのかわからない、そしてどう対処すれば良いのか悩んでいる人に読んでほしい。
edge-to-edgeとは
まずは edge-to-edge が何かを知る必要がある。
edge-to-edge とは以下の仕様が適用されている状態のこと。
- 3ボタンナビゲーションバーが半透明
- ジェスチャーナビゲーションバーが透明
- ステータスバーが透明
- コンテンツは、インセットかパディングが適用されない限り、ナビゲーション バー、ステータスバー、キャプション バーなどのシステムバーの背後に描画される
💡 インセットって何?
画面の端に配置されたシステムのUI(ステータスバーやナビゲーションバーなど) に隠れてしまう領域のこと。
上記は公式ドキュメントの文章ほぼそのままだが、わかるようでわからないだろう。 次の章で edge-to-edge が有効のケースと無効のケースを比較することで edge-to-edge が何かが見えてくるはず。
edge-to-edge が有効になるケースは?
Android 端末、API レベル、enableEdgeToEdge()
の使用・未使用の組み合わせによって、アプリが edge-to-edge(画面端までコンテンツを表示するデザイン)になるかどうかを検証してみる。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 34 以下で edge-to-edge にする場合
setContent {
HelloedgeTheme {
Greeting(
name = "Android",
)
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(
text = "Hello $name!",
)
}
}
Android 14 端末(Pixel8)
Android 14 端末は enableEdgeToEdge()
が有効の場合のみ edge-to-edge が有効になる。
-
API 34 +
enableEdgeToEdge()
未使用 -
API 34 +
enableEdgeToEdge()
使用 -
API 35 +
enableEdgeToEdge()
未使用 -
API 35 +
enableEdgeToEdge()
使用edge-to-edge が有効。
Android 15 端末(Pixel8)
Android 15 端末は enableEdgeToEdge()
が有効の場合に加え、API 35 の場合に edge-to-edge が有効となる。
-
API 34 +
enableEdgeToEdge()
未使用 -
API 34 +
enableEdgeToEdge()
使用edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
-
API 35 +
enableEdgeToEdge()
未使用edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
-
API 35 +
enableEdgeToEdge()
使用edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
ターゲット API 35 にアップデートすることによる影響
ターゲット API が 34 以下の既存アプリにおいて edge-to-edge を意識していないもしくは edge-to-edge にしたくない状態、つまりenableEdgeToEdge()
未使用状態の場合、ターゲット API を 35 にアップデートするとAndroid 15 端末で edge-to-edge になってしまう。
💡
enableEdgeToEdge()
は androidx.activity:activity- ktx:1.5.0-alpha01 から使用可能。1.5.0-alpha01 は Jan 26, 2022 にリリースされている。
💡
enableEdgeToEdge()
を使用せずに edge-to-edge にする方法は Manually set up the edge-to-edge display を参照。
Scaffold を使用している場合
ページのトップ Composable 関数に Scaffold
を配置し、さらにScaffold
の引数 content
の PaddingValues
を直下の Composable 関数の modifier に渡していれば、 edge-to-edge の対応していることになる(ここ参照)。 現時点でScaffold
の PaddingValues
を意識したコーディングができていればターゲット API 35 へのアップデートによって edge-to-edge が意図せず有効になっても慌てることはないはずである。
本当に Scaffold
で edge-to-edge 対策になっているのか不安なので検証しつつ、引数 content
の PaddingValues
には edge-to-edge の有効・無効状態によって何が入るのか確認してみる。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 34 以下で edge-to-edge にする場合
setContent {
HelloedgeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(
text = "Hello $name!",
)
}
}
Android 14 端末(Pixel8)
-
API 34 +
enableEdgeToEdge()
未使用start=0.0dp, top=0.0, end=0.0dp, bottom=0.0,
padding
が0.0
であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。 -
API 34 +
enableEdgeToEdge()
使用start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0
edge-to-edge が有効であるが、
Scaffold
のmodifier
にpadding.top=50.285713
を指定するのでコンテンツとステータスバーが被らない。 -
API 35 +
enableEdgeToEdge()
未使用start=0.0dp, top=0.0, end=0.0dp, bottom=0.0,
padding
が0.0
であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。 -
API 35 +
enableEdgeToEdge()
使用start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0
edge-to-edge が有効であるが、
Scaffold
のmodifier
にpadding.top=50.285713
を指定するのでコンテンツとステータスバーが被らない。
Android 15(Pixel8)
Android 15 端末は enableEdgeToEdge()
が有効の場合に加え、API 35 の場合に edge-to-edge が有効になる。
-
API 34 +
enableEdgeToEdge()
未使用start=0.0dp, top=0.0, end=0.0dp, bottom=0.0
padding
が0.0
であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。ただ、ステータスバーがカメラ領域を考慮しない。 -
API 34 +
enableEdgeToEdge()
使用start=0.0dp, top=24.0, end=0.0dp, bottom=24.0
edge-to-edge が有効であるが、
Scaffold
のmodifier
にpadding.top=24
を指定するのでコンテンツとステータスバーが被らない。ただし、 Android 15 端末はカメラ領域を考慮しないので、padding.top=24
はカメラ領域を考慮する場合の50.285713
よりも小さくなっている。 -
API 35 +
enableEdgeToEdge()
未使用start=0.0dp, top=24.0, end=0.0dp, bottom=24.0
edge-to-edge が有効であるが、
Scaffold
のmodifier
にpadding.top=24
を指定するのでコンテンツとステータスバーが被らない。ただし、 Android 15 端末はカメラ領域を考慮しないので、padding.top=24
はカメラ領域を考慮する場合の50.285713
よりも小さくなっている。 -
API 35 +
enableEdgeToEdge()
使用start=0.0dp, top=24.0, end=0.0dp, bottom=24.0
edge-to-edge が有効であるが、
Scaffold
のmodifier
にpadding.top=24
を指定するのでコンテンツとステータスバーが被らない。ただし、 Android 15 端末はカメラ領域を考慮しないので、padding.top=24
はカメラ領域を考慮する場合の50.285713
よりも小さくなっている。
ターゲット API 35 にバージョンアップすることによる影響
Scaffold
の引数 content
の PaddingValues
を正しく使用していればターゲット API 35 にバージョンアップして edge-to-edge が有効になってもステータスバーやナビゲーションバーがコンテンツと被ることはない。
Scaffold(topBar= ,bottomBar= )
は大丈夫なの?
edge-to-edge が有効、かつ Scaffold(topBar= ,bottomBar= )
でアプリバーとボトムバーを描画する場合はどうなるのだろうか? content
の PaddingValues
にはアプリバーとボトムバー領域を考慮した padding がちゃんと設定されるのだろうか。以下のサンプルプログラムで検証してみる。
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 34 以下で edge-to-edge にする場合
setContent {
EdgetoedgeTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar ={
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text("Top app bar")
}
)
},
bottomBar = {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.primary,
) {
Text(
modifier = Modifier
.fillMaxWidth(),
textAlign = TextAlign.Center,
text = "Bottom app bar",
)
}
}
) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
アプリバーとボトムバーはステータスバーやナビゲーションバーに被ることはなく、コンテンツはアプリバーとボトムバーの内側に表示される。
PaddingVlues
は
start=0.0dp, top=88.0, end=0.0dp, bottom=104.0
であった(Pixel8の場合)。アプリバーの高さ 64dp
とボトムバーの高さ 80dp
が追加されたようだ。
Scaffold を使用せずにステータスバー・ナビゲーションバーとコンテンツが被らないようにする
Scaffold
を使用していない場合はどうやってステータスバー・ナビゲーションバーとコンテンツが被らないようにすれば良いのだろうか。
Modifier.safeDrawingPadding()
これが一番手っ取り早いようだ。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 35 化時に Android 14 端末も edge-to-edge にする場合
setContent {
HelloedgeTheme {
Greeting(
name = "Android",
)
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(modifier = modifier.safeDrawingPadding()) {
Text(
text = "Hello $name!",
)
}
}
ステータスバーを避けてコンテンツが表示された。では、safeDrawingPadding
のコンテンツ領域の正体を見てみる。
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(modifier = modifier.safeDrawingPadding().fillMaxSize().background(Color.Cyan)) {
Text(
text = "Hello $name!",
)
}
}
確かに safeDrawingPadding
を使えばステータスバーやナビゲーションバーがコンテンツに被らないようだ。
その他の方法
実は Modifier.safeDrawingPadding()
はModifier.windowInsetsPadding(WindowInsets.safeDrawing)
と同じである。WindowInsets.safeDrawing
の他に (詳細はここ)
を指定することも可能である。ステータスバーのみコンテンツに被らないようにするには以下のようにする
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.windowInsetsPadding(WindowInsets.statusBars)
.fillMaxSize().background(Color.Cyan)
) {
Text(
text = "Hello $name!",
)
}
}
【課題】 ステータスバー・ナビゲーションバーとコンテンツが被らないようにはできたが...
Scaffold
の PaddingValues
、または Modifier.safeDrawingPadding()
を使うことによってステータスバー・ナビゲーションバーとコンテンツが被ることはなくなるが、 edge-to-edge が有効な場合はステータスバーとナビゲーションバーが透明であり、透明のままにしておくと edge-to-edge である意味がない。edge-to-edge であるなら以下のようにステータスバー・ナビゲーションバーの背景を描画し、コンテンツの背景とステータスバー・ナビゲーションバーの背景をシームレスにつなげたい。
ステータスバー・ナビゲーションバーの背景描画方法はこの記事では紹介しないので調べて欲しい。
Scaffold
でステータスバー・ナビゲーションバーの背景を描画する記事を書いてみたのでもし良かったら参考にしてみて欲しい。
【課題】 edge-to-edge 対処は ステータスバー・ナビゲーションバーとコンテンツが被らないようにすればいいだけではない?
ステータスバー・ナビゲーションバーとコンテンツが被らないようにすれば edge-to-edge 対処が完了するわけではないらしい。例を以下に示す。
3ボタンナビゲーションの背景が半透明
ナビゲーションバーの背景を設定し、ナビゲーションモードが3ボタンナビゲーションの場合とジェスチャーナビゲーションの場合で比較してみる。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 35 化時に Android 14 端末も edge-to-edge にする場合
setContent {
EdgetoedgeTheme {
Greeting(name = "Android")
}
}
}
}
@Composable
private fun Greeting(
name: String,
modifier: Modifier = Modifier,
) {
Box {
// 背景用の Composable 関数
Column(
modifier = Modifier.fillMaxSize().background(color = Color.Cyan)
) {}
// コンテンツ用の Composable 関数
Column(
modifier = modifier
.fillMaxSize()
.safeDrawingPadding()
.background(color = Color.Cyan)
) {
Text(
text = "Hello $name!",
)
}
}
}
3ボタンナビゲーションの背景が半透明になっているのがお分かりだろうか。半透明を解消し、指定した通りの背景にするには window.isNavigationBarContrastEnforced
プロパティを false
にする。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // API 35 化時に Android 14 端末も edge-to-edge にする場合
setContent {
EdgetoedgeTheme {
Greeting(name = "Android")
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ window.isNavigationBarContrastEnforced = false
+ }
}
}
}
3ボタンナビゲーションの背景が半透明になる事象が解消された。