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

Android 15 の動作変更点の一つである [Edge-to-edge enforcement](https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge) を読みましたか？ アプリの対象 API レベル 35 に上げると edge-to-edge が強制適用されるとのこと。これまで edge-to-edge を無視して来てしまい、既存アプリに edge-to-edge が適用されたらどうなってしまうのかわからない、そしてどう対処すれば良いのか悩んでいる人に読んでほしい。

## edge-to-edgeとは

まずは edge-to-edge が何かを知る必要がある。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/4fc36036-a190-9a83-38a4-8a6402a50e27.png" alt="" width="300">

edge-to-edge とは以下の仕様が適用されている状態のこと。

- 3ボタンナビゲーションバーが半透明
- ジェスチャーナビゲーションバーが透明
- ステータスバーが透明
- コンテンツは、インセットかパディングが適用されない限り、ナビゲーション バー、ステータスバー、キャプション バーなどのシステムバーの背後に描画される


> 💡 インセットって何？
> 画面の端に配置されたシステムのUI（ステータスバーやナビゲーションバーなど） に隠れてしまう領域のこと。

上記は公式ドキュメントの文章ほぼそのままだが、わかるようでわからないだろう。 次の章で edge-to-edge が有効のケースと無効のケースを比較することで edge-to-edge が何かが見えてくるはず。

## edge-to-edge が有効になるケースは？

Android 端末、API レベル、[`enableEdgeToEdge()`](https://developer.android.com/reference/androidx/activity/ComponentActivity#(androidx.activity.ComponentActivity).enableEdgeToEdge(androidx.activity.SystemBarStyle,androidx.activity.SystemBarStyle)) の使用・未使用の組み合わせによって、アプリが edge-to-edge（画面端までコンテンツを表示するデザイン）になるかどうかを検証してみる。

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

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/fc0e62cf-5d0c-148b-6fbf-b8e58abce966.png" alt="" width="300">
    
- API 34 + `enableEdgeToEdge()` 使用
    
    edge-to-edge が有効。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/53f44962-6a99-e5a2-f914-8d11460cb2ba.png" alt="" width="300">
    
- API 35 + `enableEdgeToEdge()` 未使用

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/1bf90a14-ad6b-0d71-4a92-32c2be391303.png" alt="" width="300">
    
- API 35 + `enableEdgeToEdge()` 使用
    
    edge-to-edge が有効。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/98ffe81c-f7d9-e654-7f2b-fb126a01c127.png" alt="" width="300">    

### Android 15 端末(Pixel8)

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

- API 34 + `enableEdgeToEdge()` 未使用
    
    ステータスバー領域にカメラ領域が含まれていないので、 Android 14 よりも狭い。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/aa294e48-759d-eaaa-93e1-f12b9d0b94a8.png" alt="" width="300">
        
- API 34 + `enableEdgeToEdge()` 使用
    
    edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/93493d78-281d-ad9e-493c-bcfc1f6b9da9.png" alt="" width="300">
    
- **API 35** + `enableEdgeToEdge()` 未使用
    
    edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/d87e6e8a-0237-77f2-f3a0-abaab30a4076.png" alt="" width="300">
    
- **API 35** + `enableEdgeToEdge()` 使用
    
    edge-to-edge が有効。ただ、コンテンツとステータスバーが被っているが Android 14 端末と微妙に異なる。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/d87e6e8a-0237-77f2-f3a0-abaab30a4076.png" alt="" width="300">
        

### ターゲット 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](https://developer.android.com/develop/ui/views/layout/edge-to-edge-manually) を参照。

## Scaffold を使用している場合

ページのトップ Composable 関数に [`Scaffold`](https://developer.android.com/develop/ui/compose/components/scaffold) を配置し、さらに[`Scaffold`](https://developer.android.com/develop/ui/compose/components/scaffold) の引数 `content` の `PaddingValues` を直下の Composable 関数の modifier に渡していれば、 edge-to-edge の対応していることになる([ここ](https://developer.android.com/develop/ui/compose/layouts/insets#scaffold)参照)。 現時点で`Scaffold` の `PaddingValues` を意識したコーディングができていればターゲット API 35 へのアップデートによって edge-to-edge が意図せず有効になっても慌てることはないはずである。

本当に `Scaffold` で edge-to-edge 対策になっているのか不安なので検証しつつ、引数 `content` の `PaddingValues` には edge-to-edge の有効・無効状態によって何が入るのか確認してみる。


```kotlin
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()` 未使用
    
    ```kotlin
    start=0.0dp, top=0.0, end=0.0dp, bottom=0.0, 
    ```
    
    `padding` が `0.0` であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/6c70967d-b1da-a00e-13fd-4312636366b2.png" alt="" width="300">
    
- API 34 + `enableEdgeToEdge()` 使用
    
    ```kotlin
    start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0 
    ```
    
    edge-to-edge が有効であるが、`Scaffold` の `modifier` に `padding.top=50.285713` を指定するのでコンテンツとステータスバーが被らない。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/1f36f30f-976d-0662-cdfd-62d3b4a9367e.png" alt="" width="300">
            
- API 35 + `enableEdgeToEdge()` 未使用
    
    ```kotlin
    start=0.0dp, top=0.0, end=0.0dp, bottom=0.0, 
    ```
    
    `padding` が `0.0` であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。
    
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/296ed2c8-70ec-f722-7c1f-22c6517217dc.png" alt="" width="300">

- API 35 + `enableEdgeToEdge()` 使用
    
    ```kotlin
    start=0.0dp, top=50.285713, end=0.0dp, bottom=24.0
    ```
    
    edge-to-edge が有効であるが、`Scaffold` の `modifier` に `padding.top=50.285713` を指定するのでコンテンツとステータスバーが被らない。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/095ec935-4b17-7607-6e0a-cecb6424795a.png" alt="" width="300">    

### Android 15(Pixel8)

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

- API 34 + `enableEdgeToEdge()` 未使用
    
    ```kotlin
    start=0.0dp, top=0.0, end=0.0dp, bottom=0.0
    ```
    
    `padding` が `0.0` であるが、edge-to-edge が無効なのでコンテンツとステータスバーが被らない。ただ、ステータスバーがカメラ領域を考慮しない。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/976ea256-ef61-28dc-9b12-f7b30050500c.png" alt="" width="300">
    
- API 34 + `enableEdgeToEdge()` 使用
    
    ```kotlin
    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` よりも小さくなっている。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/ff51fa81-970f-2481-41a7-d837d6d5c216.png" alt="" width="300">
    
- **API 35** + `enableEdgeToEdge()` 未使用
    
    ```kotlin
    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` よりも小さくなっている。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/2a05172a-be28-c723-c517-24e17cbc7f8b.png" alt="" width="300">
    
- **API 35** + `enableEdgeToEdge()` 使用
    
    ```kotlin
    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` よりも小さくなっている。

    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/31e1f4f8-ef5f-8f95-76db-67a4fa665586.png" alt="" width="300">

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

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

### `Scaffold(topBar= ,bottomBar= )` は大丈夫なの？

edge-to-edge が有効、かつ `Scaffold(topBar= ,bottomBar= )` でアプリバーとボトムバーを描画する場合はどうなるのだろうか？ `content` の `PaddingValues` にはアプリバーとボトムバー領域を考慮した padding がちゃんと設定されるのだろうか。以下のサンプルプログラムで検証してみる。


```kotlin
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)
                    )
                }
            }
        }
    }
}
```

アプリバーとボトムバーはステータスバーやナビゲーションバーに被ることはなく、コンテンツはアプリバーとボトムバーの内側に表示される。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/5e960eb8-fcf6-ae91-aa0a-16054f9553fb.png" alt="" width="200">

`PaddingVlues` は

```kotlin
start=0.0dp, top=88.0, end=0.0dp, bottom=104.0
```

であった(Pixel8の場合)。アプリバーの高さ `64dp` とボトムバーの高さ `80dp` が追加されたようだ。


## Scaffold を使用せずにステータスバー・ナビゲーションバーとコンテンツが被らないようにする

`Scaffold` を使用していない場合はどうやってステータスバー・ナビゲーションバーとコンテンツが被らないようにすれば良いのだろうか。

### [`Modifier.safeDrawingPadding()`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).safeDrawingPadding())

これが一番手っ取り早いようだ。

```kotlin
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!",
        )
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/e23ac103-e961-12b6-fa66-64c178875653.png" alt="" width="300">

ステータスバーを避けてコンテンツが表示された。では、`safeDrawingPadding` のコンテンツ領域の正体を見てみる。

```kotlin
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column(modifier = modifier.safeDrawingPadding().fillMaxSize().background(Color.Cyan)) {
        Text(
            text = "Hello $name!",
        )
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/2eedfe80-3be1-e9ca-a807-ebd653f463f1.png" alt="" width="200">

確かに `safeDrawingPadding` を使えばステータスバーやナビゲーションバーがコンテンツに被らないようだ。

### その他の方法

実は [`Modifier.safeDrawingPadding()`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).safeDrawingPadding())は`Modifier.windowInsetsPadding(WindowInsets.safeDrawing)` と同じである。`WindowInsets.safeDrawing`の他に (詳細は[ここ](https://developer.android.com/develop/ui/compose/layouts/insets#inset-fundamentals))

- [`WindowInsets.systemBars`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).systemBars())
- [`WindowInsets.statusBars`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).statusBars())
- [`WindowInsets.navigationBars`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).navigationBars())

を指定することも可能である。ステータスバーのみコンテンツに被らないようにするには以下のようにする

```kotlin
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .windowInsetsPadding(WindowInsets.statusBars)
            .fillMaxSize().background(Color.Cyan)
    ) {
        Text(
            text = "Hello $name!",
        )
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/97121920-ae2f-2244-2a3e-fa84553a8e8f.png" alt="" width="200">

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

`Scaffold` の `PaddingValues` 、または `Modifier.safeDrawingPadding()` を使うことによってステータスバー・ナビゲーションバーとコンテンツが被ることはなくなるが、 edge-to-edge が有効な場合はステータスバーとナビゲーションバーが透明であり、透明のままにしておくと edge-to-edge である意味がない。edge-to-edge であるなら以下のようにステータスバー・ナビゲーションバーの背景を描画し、コンテンツの背景とステータスバー・ナビゲーションバーの背景をシームレスにつなげたい。
    
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/4e870cc6-e56a-6090-59eb-cdeb746bcc20.png" alt="" width="300">

ステータスバー・ナビゲーションバーの背景描画方法はこの記事では紹介しないので調べて欲しい。
`Scaffold` でステータスバー・ナビゲーションバーの背景を描画する記事を書いてみたのでもし良かったら参考にしてみて欲しい。

https://qiita.com/seabat-dev/items/4b8d652f92aa2343a7ce


## 【課題】 edge-to-edge 対処は ステータスバー・ナビゲーションバーとコンテンツが被らないようにすればいいだけではない？

ステータスバー・ナビゲーションバーとコンテンツが被らないようにすれば edge-to-edge 対処が完了するわけではないらしい。例を以下に示す。

#### 3ボタンナビゲーションの背景が半透明

ナビゲーションバーの背景を設定し、ナビゲーションモードが3ボタンナビゲーションの場合とジェスチャーナビゲーションの場合で比較してみる。

```kotlin
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!",
            )
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/db48b112-e658-db41-400c-37f58be3ca6c.png" alt="" width="200"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/71a3cf9d-2e96-126b-b2e6-32eb7662f663.png" alt="" width="200">

3ボタンナビゲーションの背景が半透明になっているのがお分かりだろうか。半透明を解消し、指定した通りの背景にするには [`window.isNavigationBarContrastEnforced`](https://developer.android.com/reference/android/view/Window#isNavigationBarContrastEnforced()) プロパティを `false` にする。

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/db5f9814-1384-323c-988f-4f9e84abbf1a.png" alt="" width="200"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/71a3cf9d-2e96-126b-b2e6-32eb7662f663.png" alt="" width="200">