9
10

アプリの対象 API レベル 35 で初めて edge-to-edge に対処する[Compose編]

Last updated at Posted at 2024-07-19

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() 使用

    edge-to-edge が有効。

  • API 35 + enableEdgeToEdge() 未使用

  • API 35 + enableEdgeToEdge() 使用

    edge-to-edge が有効。

Android 15 端末(Pixel8)

Android 15 端末は enableEdgeToEdge() が有効の場合に加え、API 35 の場合に edge-to-edge が有効となる。

  • API 34 + enableEdgeToEdge() 未使用

    ステータスバー領域にカメラ領域が含まれていないので、 Android 14 よりも狭い。

  • 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 の引数 contentPaddingValues を直下の Composable 関数の modifier に渡していれば、 edge-to-edge の対応していることになる(ここ参照)。 現時点でScaffoldPaddingValues を意識したコーディングができていればターゲット API 35 へのアップデートによって edge-to-edge が意図せず有効になっても慌てることはないはずである。

本当に Scaffold で edge-to-edge 対策になっているのか不安なので検証しつつ、引数 contentPaddingValues には 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, 
    

    padding0.0 であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。

  • API 34 + enableEdgeToEdge() 使用

    start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0 
    

    edge-to-edge が有効であるが、Scaffoldmodifierpadding.top=50.285713 を指定するのでコンテンツとステータスバーが被らない。

  • API 35 + enableEdgeToEdge() 未使用

    start=0.0dp, top=0.0, end=0.0dp, bottom=0.0, 
    

    padding0.0 であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。

  • API 35 + enableEdgeToEdge() 使用

    start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0
    

    edge-to-edge が有効であるが、Scaffoldmodifierpadding.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
    

    padding0.0 であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。ただ、ステータスバーがカメラ領域を考慮しない。

  • API 34 + enableEdgeToEdge() 使用

    start=0.0dp, top=24.0, end=0.0dp, bottom=24.0
    

    edge-to-edge が有効であるが、Scaffoldmodifierpadding.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 が有効であるが、Scaffoldmodifierpadding.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 が有効であるが、Scaffoldmodifierpadding.top=24 を指定するのでコンテンツとステータスバーが被らない。ただし、 Android 15 端末はカメラ領域を考慮しないので、padding.top=24 はカメラ領域を考慮する場合の 50.285713 よりも小さくなっている。

ターゲット API 35 にバージョンアップすることによる影響

Scaffold の引数 contentPaddingValues を正しく使用していればターゲット API 35 にバージョンアップして edge-to-edge が有効になってもステータスバーやナビゲーションバーがコンテンツと被ることはない。

Scaffold(topBar= ,bottomBar= ) は大丈夫なの?

edge-to-edge が有効、かつ Scaffold(topBar= ,bottomBar= ) でアプリバーとボトムバーを描画する場合はどうなるのだろうか? contentPaddingValues にはアプリバーとボトムバー領域を考慮した 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!",
        )
    }
}

【課題】 ステータスバー・ナビゲーションバーとコンテンツが被らないようにはできたが...

ScaffoldPaddingValues 、または 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ボタンナビゲーションの背景が半透明になる事象が解消された。

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10