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

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()` の使用・未使用の組み合わせによって、アプリが edge-to-edge（画面端までコンテンツを表示するデザイン）になるかどうかを検証してみる。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge() // API 34 以下で edge-to-edge にする場合
        setContentView(R.layout.activity_main)
    }
}
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#e1f6cf"
    tools:context=".MainActivity">
    
    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Hello Android!"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  </androidx.constraintlayout.widget.ConstraintLayout>
  ```

  ```xml:themes.xml
  <resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Base.Theme.EdgeXml" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- Customize your light theme here. -->
        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
    </style>

    <style name="Theme.EdgeXml" parent="Base.Theme.EdgeXml" />
</resources>
  ```

  ### 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/acec726e-1b8a-a247-ca65-9ac28e9614a7.png" alt="" width="300">
    
- API 34 + **`enableEdgeToEdge()` 有効**
    
    edge-to-edge が有効。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/464a63cf-f4b3-ffda-70a8-5a58f0d8d6d5.png" alt="" width="300">
    
- API 35 + `enableEdgeToEdge()` 無効
    
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/46cc92ad-aad5-ee1c-b1d7-6a145fdbb5cd.png" alt="" width="300">
    
- API 35 + **`enableEdgeToEdge()` 有効**
    
    edge-to-edge が有効。
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/b688038b-3be3-95ef-ab36-9b72e4674749.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/b06a5848-a708-e905-72f5-00c6818a6f2d.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/46dfc0bc-8d56-7f81-fbe3-040bf176092f.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/288de442-0c81-ef68-bf27-2c3520cebd62.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/8fdd1355-faa2-3bee-292f-c38163cc0245.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) を参照。

## edge-to-edge 有効時にステータスバー・ナビゲーションバーとコンテンツが被らないようにする

公式ドキュメント [インセットを使用して重なりを処理する](https://developer.android.com/develop/ui/views/layout/edge-to-edge?hl=ja#handle-overlaps) の「システムバーのインセット」について説明する。 公式ドキュメントは FAB という特殊な場合しか説明してくれないので、ここでは「ステータスバー・ナビゲーションバーとコンテンツを被らないようにするにはどうすれば良いのか？」という最も基本的なところを説明をしたい。

ステータスバー・ナビゲーションバーとコンテンツが被らないようにする。

```diff
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge() // API 35 化時に Android 14 端末も edge-to-edge にする場合
        setContentView(R.layout.activity_main)
+       ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+           val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+           v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+           insets
+       }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/5b802b3b-6073-bfb1-5379-9d604c05cf95.png" alt="" width="300">

activity_main.xml の Top レベルに配置した `ConstraintLayout` はステータスバー領域に表示されるが、その子ビューは ステータスバーに被らないように表示される。
ページの一番下に `<TextView>` を配置し、ナビゲーションバー領域はどのように表示されるのを確認してみる。

```xml：activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...      
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello Android!"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

  </androidx.constraintlayout.widget.ConstraintLayout>
  ```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/ecfee701-6665-a7b5-c762-e8f7355d4317.png" alt="" width="300">

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

公式ドキュメント [ディスプレイ カットアウト インセット](https://developer.android.com/develop/ui/views/layout/edge-to-edge?hl=ja#cutout-insets)や [システム ジェスチャー インセット](https://developer.android.com/develop/ui/views/layout/edge-to-edge?hl=ja#system-gesture-insets)の記載があり、ステータスバー・ナビゲーションバーとコンテンツが被らないようにするだけで edge-to-edge 対処が完了するわけではない。例を以下に示す。

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

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

```kotlin:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#e1f6cf" //ステータスバー・ナビゲーションバーを含めた全背景
    tools:context=".MainActivity">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello Android!"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/8ab47936-11f8-fe58-b396-aa62b31c4c4e.png" alt="" width="200"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/a9bbca18-d0fb-69b9-c231-d19f3a8b1036.png" alt="" width="200">

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

```diff
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge() // API 35 化時に Android 14 端末も edge-to-edge にする場合
        setContentView(R.layout.activity_android_view)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

+       // 3ボタンナビゲーションの背景の半透明を無効化する
+       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/c7fe2c35-9fc8-bae5-c7ca-e550ca2a8b70.png" alt="" width="200"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494458/a9bbca18-d0fb-69b9-c231-d19f3a8b1036.png" alt="" width="200">