Android Edge-to-edge入門

この記事は概ね、[Gesture Navigation: Going edge-to-edge (I)](https://medium.com/androiddevelopers/gesture-navigation-going-edge-to-edge-812f62e4e83e)と[Gesture Navigation: Handling visual overlaps (II)](https://medium.com/androiddevelopers/gesture-navigation-handling-visual-overlaps-4aed565c134c)の記事に沿って書いています。

---
Android Qから新しいシステムナビゲーションが追加されました。
iOSではおなじみですが、ボタンではなくジェスチャーによって前の画面に戻ったりAndroidのホーム画面に遷移することが可能になりました。
<img width="256" alt="gesture.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/802bb67d-a8c7-1a6a-aa46-fe8d326cf33f.gif">
[*ジェスチャー ナビゲーション: エッジ ツー エッジへの対応より*](https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html)

このジェスチャーナビゲーションを利用することで、従来の3ボタンナビゲーションよりも多くの描画領域をアプリに提供できるため、より没入感のあるUXを提供することができるようになりました。

# Edge-to-edgeとは

いわゆるこれです。
<img width="256" alt="edgetoedge.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/251b372f-ea01-64fb-ef43-fcdad20339cc.gif">
[*ジェスチャー ナビゲーション: エッジ ツー エッジへの対応より*](https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html)

よりアプリへの没入感の高いUXを提供できる表現のことをこう呼びます。
より詳しくはぜひGoogle Developersのブログを御覧ください。

*Gesture Navigation: Going edge-to-edge (I)*に関しては[日本語訳記事](https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html)もあるので一読すると理解が深まります！

# 実装方法

## 前提

本記事は`androidx.core:core-ktx:1.2.0-rc01`を用いてコードの検証を行っています。
また、コードの中でデータバインディングを用いている箇所があります。

```
dependencies {
    ...
    implementation 'androidx.core:core-ktx:1.2.0-rc01'
}
```

題材として、適当な文字列をリストで表示。またFloatingActionButtonのある以下のようなUIをEdge-to-edge対応させていく流れで進めてみます。
<img width="512" alt="2019-11-30 22.54.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/8bba9121-1bbf-92ca-bed4-93791f1e75bb.png">
普通に作るとこんな感じ。コードはすべて[こちら](https://github.com/rmakiyama/IntroductionToEdgeToEdge)に。
左はAPI 27、右はAPI 29です。
ブランチを変えるとそれぞれのステップに対応しています。

## 全画面表示に対応する

まずはステータスバーの背後にアプリを描画できるようにしましょう。
これ自体は非常に簡単で、`systemUiVisibility`にflagを設定してあげるだけです。

```MainActivity.kt
view.systemUiVisibility =
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
```

<img width="512" alt="スクリーンショット 2019-11-30 22.59.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/4b42a725-2071-001b-8b6f-1c4f4595cf46.png">
善し悪しは置いておいて、ステータスバー・ナビゲーションバーの背後にUIが配置されています。

ステータスバーの背後のみや、ナビゲーションバーの背後のみということも可能です。
自分のアプリに適した設定にしましょう。

## システムバーの色を変える

次に重なって表示されたUIを見えるようにするため、透過された色をシステムバーに設定しましょう。
Android Qでは単純に透明に設定するのみでよいです。システムが動的に色を調整してくれます。
Pie以前では動的な色の調整は行われないため、半透明に設定することが推奨されています。
その際、不透明度が70%から始め、各自のアプリに表示されるコンテンツに応じて調整することが推奨されています。

```resources.xml
<!-- styles.xml -->
<resources>
    <style name="Base.App" parent="Theme.MaterialComponents.Light.NoActionBar">
        ...

        <item name="android:navigationBarColor">@color/nav_bar</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>

<!-- values/colors.xml -->

<resources>
    ...

    <color name="light_scrim">#B3FFFFFF</color>
    <color name="nav_bar">@color/light_scrim</color>
</resources>

<!-- values-v29/colors.xml -->
<resources>
    <color name="nav_bar">@android:color/transparent</color>
</resources>
```

<img width="512" alt="スクリーンショット 2019-11-30 23.13.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/8577115f-07e9-870c-0fc9-65f428bb9716.png">
描画領域が広がったことがわかるかと思います。

## 視覚的な重なりに対応する

最後に、システムバーの背後に描画されたUIについて適切に対応していきましょう。肝です。

たとえば、FloatingActionButtonを使っている場合、ボタンがナビゲーションバーと重なってしまっていることに気づくでしょう。
あるいは、このようにリストを表示している場合、リストの最後の要素がナビゲーションバーに重なってしまい、見づらくなってしまっています。

### よくない対応

よくない対応として、それぞれのマージンあるいはパッディングにシステムバー分の高さを足すパターンがあります。
システムバーはAndroidのバージョンや端末により異なる場合場あるので、そこを考慮していない場合は表示が崩れることになります。今後のことを考えても、これは避けたほうが良いでしょう。

<img width="512" alt="スクリーンショット 2019-11-30 23.15.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/40328b62-807d-6872-267d-ef2636bb85d1.png">
これは、`layout_marginBottom`を、ナビゲーションバーの48dpとFABのマージンの16dpを足して、64dpとした時のスクリーンショットです。
API 27では一見良さそうですが、API 29でみると崩れていますね。

### 適切な対応

System window insetsを利用し、適切に対応していきましょう。
WindowInsetについては詳しく説明しません。[こちら](https://qiita.com/takahirom/items/c0864ad6ec2a6c013552)に非常に丁寧に説明されているのでぜひ御覧ください。

`ViewCompat`の`setOnApplyWindowInsetsListener`から`WindowInset`にアクセスできるので、ここでViewのマージンあるはパッディングを調整しましょう。

```MainActivity.kt
private fun handlingInsets(view: View) {
    // リソースからマージンを取得
    val fabMargin = resources.getDimensionPixelSize(R.dimen.fab_margin)
    // XML等から既にviewで指定されているpaddingを取得
    val listBottomPadding = binding.listText.paddingBottom
    // WindowInsetsにアクセス
    ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
        // マージンを更新するときはこんな感じ
        binding.toolbar.updateLayoutParams<AppBarLayout.LayoutParams> {
            topMargin = insets.systemWindowInsetTop
        }
        binding.fab.updateLayoutParams<CoordinatorLayout.LayoutParams> {
            leftMargin = fabMargin + insets.systemWindowInsetLeft
            rightMargin = fabMargin + insets.systemWindowInsetRight
            bottomMargin = fabMargin + insets.systemWindowInsetBottom
        }
        // パッディングを更新するときはこんな感じ
        binding.listText.updatePadding(
            bottom = insets.systemWindowInsetBottom + listBottomPadding
        )
        insets
    }
}
```

<img width="512" alt="スクリーンショット 2019-11-30 23.36.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/25cba7b2-3300-016f-7b05-2f7c43b56357.png">
WindowInsetsがバージョン毎のサイズを適切に判断してくれるので、それぞれきれいにマージンが指定できました。

## その他の対応方法

上記で説明した流れは、[Insetter](https://github.com/chrisbanes/insetter)というライブラリで一貫して対応が可能です。
ここまで説明しといてなんですが、かなり便利なのでこちらを使うのもオススメです。

[簡単に解説したスライド](https://speakerdeck.com/rmakiyama/edge-to-edge-with-insetter)を公開しているので、参考までに。
# まとめ

・入門は簡単だけど、それぞれでWindowInsetsに対応するのに骨が折れそう
・UXは高まるので対応していきたい。