Trackrアプリから学ぶ大画面対応

この記事は[Android Advent Calendar 2021](https://qiita.com/advent-calendar/2021/android) 22日目の記事です。

Android12Lもベータがリリースされ、タブレットや折りたたみ式デバイスなど大画面対応の機運も高まってきているように感じます。
この記事では、そんな大画面対応をGoogleが公開しているTrackrアプリでどのように対応・実装されているのかについて紹介していきます。

# Trackrについて
https://github.com/android/trackr

Trackrはサンプルのタスク管理アプリで、新規タスクの追加や編集、タスクのアーカイブ、タスクのお気に入りといった定番の機能を持っています。モダンな技術を利用しつつ、アクセシビリティの観点からも実装が行われている他、Composeでの実装パターンも開発が進んでいます。
今回はその中で大画面対応にスポットを当てて紹介します。

# Navigation Rail

https://material.io/components/navigation-rail

https://m3.material.io/components/navigation-rail/overview

Navigation Railは、3~7画面のナビゲーションを設けることのできるコンポーネントで、Bottom Navigationが画面下に配置されるのに対して、画面横に配置されるため大きな画面サイズでもスペースを有効活用できます。


## 実装
Trackrアプリでは、レイアウトリソースを分けることで、w600dp以上の画面の場合にはNavigation Railが利用され、それ以下の場合には、Bottom App Barが利用されるようになっています。

https://github.com/android/trackr/blob/main/app/src/main/res/layout/main_activity.xml

https://github.com/android/trackr/blob/main/app/src/main/res/layout-w600dp/main_activity.xml

| Bottom App Bar | Navigation Rail |
|-|-|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/3d4b356b-9959-0b06-eaa6-ce4b0a338280.png" width="300" />|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/63886a63-2e8d-5948-4c1b-ff0533b3e6cc.png" width="600" />|

Bottom Navigationと同様に `app:menu` でアイテムを設定し、`app:labelVidibilityMode` でラベルの表示有無を設定します。

```xml:main_activity.xml
<com.google.android.material.navigationrail.NavigationRailView
            android:id="@+id/navigation_rail"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:headerLayout="@layout/navigation_rail_header"
            app:labelVisibilityMode="unlabeled"
            app:menu="@menu/navigation_rail" />
```

Trackrアプリでは、Navigation RailはActivityで持っていますが、Bottom App BarはそれぞれのFragmentで保持しています。これは、Navigation　Railがトップレベルでどこからでもアクセスできるという観点で常時表示されるコンポーネントであるためと思われます。

このため、縦画面ではタスク一覧画面のみでタスク追加用のFloating Action Buttonが表示されますが、横画面などNavigation Railで表示されている場合には常時表示されるため、どの画面からもタスク追加を行うことができるようになっています。

Navigation Railで表示されているFloating Action Buttonは`app:headerLayout` パラメータで別のレイアウトを指定することで設定されています。ガイドラインでは、Floating Action Buttonをオプションで配置できるという記載になっていますが、やろうと思えば他のコンポーネントも配置できそうです。

次にKotlinコード側では、 `androidx.navigation` を利用して遷移が行われています。こちらも、Bottom Navigationで実装する場合と近いものになっていると思います。

```kotlin:MainActivty.kt
val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        binding.navigationRail?.apply {
            setupWithNavController(navController)
            setOnItemReselectedListener { } // Prevent navigating to the same item.
            setOnApplyWindowInsetsListener(null) // See above about consuming window insets.

            headerView?.setOnClickListener {
                navController.navigate(R.id.nav_task_edit_graph)
            }
        }
```

このアプリでは、 `setDecorFitsSystemWindows` することで、システムバー領域にもコンテンツが表示されるようになっています。この時、Navigation Railがステータスバーにめり込むようになりますが、この位置を調整するために以下の処理が行われ、個別にpaddingを当てて対応しています。
Navigation RailはLTRの言語の場合には左側、RTLの言語の場合には右側に配置されるため、それぞれで個別に調整が行われています。

```kotlin:MainActivity.kt
binding.navigationRail?.let { navRail ->
            binding.activityRoot.doOnApplyWindowInsets { v, insets, _, _ ->
                val isRtl = v.isRtl
                val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
                navRail.updatePadding(
                    left = if (isRtl) 0 else systemBars.left,
                    right = if (isRtl) systemBars.right else 0,
                    top = systemBars.top,
                    bottom = systemBars.bottom
                )

                // Consume either left or right insets, but not both.
                WindowInsetsCompat.Builder(insets).setInsets(
                    WindowInsetsCompat.Type.systemBars(),
                    Insets.of(
                        if (isRtl) systemBars.left else 0,
                        systemBars.top,
                        if (isRtl) 0 else systemBars.right,
                        systemBars.bottom
                    )
                ).build()

            }
        }

        WindowCompat.setDecorFitsSystemWindows(window, false)
```

# SlidingPaneLayout
https://developer.android.com/guide/topics/ui/layout/twopane

https://developer.android.com/reference/androidx/slidingpanelayout/widget/SlidingPaneLayout

SlidingPaneLayoutは大画面では2ペインのレイアウト、小さい画面では1ペインのみのレイアウトのように自動的に切り替えを行うコンポーネントです。
SlidingPaneLayoutでは、`layout_width` の合計を上回る画面幅を持つ端末で表示された場合には、並べて表示されます。画面に入りきらない場合には、スライドアニメーションとともに画面が切り替わるような挙動になります。

![record-211222233112.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/c360b91b-8cec-e9b3-920b-e3a597618c22.gif)

![record-211222233513.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/5770922a-b912-5bd1-0ef2-de0ad0b42398.gif)



## 実装
Trackrの場合には、`@dimen/list_pane_width` は360dp、`@detail_pane_width` は400dpが設定されていることから、760dp以上の横幅の場合、並べて表示される挙動となります。

https://github.com/android/trackr/blob/main/app/src/main/res/layout/tasks_two_pane_fragment.xml


```xml:tasks_two_pane_fragment.xml
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.slidingpanelayout.widget.SlidingPaneLayout
        android:id="@+id/sliding_pane_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/list_pane"
            android:name="com.example.android.trackr.ui.tasks.TasksFragment"
            android:layout_width="@dimen/list_pane_width"
            android:layout_height="match_parent"
            android:layout_weight="@dimen/list_pane_weight"
            tools:layout="@layout/tasks_fragment" />

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/detail_pane"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="@dimen/detail_pane_width"
            android:layout_height="match_parent"
            android:layout_weight="@dimen/detail_pane_weight"
            app:navGraph="@navigation/task_detail"
            tools:layout="@layout/task_detail_fragment" />

    </androidx.slidingpanelayout.widget.SlidingPaneLayout>
</layout>
```

また、詳細側には `navGraph` で詳細用のnavigationが設定されており、以下のように通知を受け取って選択したタスクが切り替わるようになっています。

```kotlin:TasksTwoPaneFragment
repeatWithViewLifecycle {
            launch {
                tasksViewModel.showTaskDetailEvents.collect {
                    if (it.isNewSelection) {
                        // Change the detail pane contents.
                        detailNavController.navigate(TaskDetailGraphDirections.toTaskDetail(it.taskId))
                    }
                    if (it.isUserSelection) {
                        // Slide the detail pane into view. If both panes are visible, this has no
                        // visible effect.
                        binding.slidingPaneLayout.openPane()
                    }
                }
            }
        }
```

# DialogFragment

https://github.com/android/trackr/blob/main/app/src/main/res/layout/task_edit_fragment.xml

https://github.com/android/trackr/blob/main/app/src/main/res/layout-large/task_edit_fragment.xml

タスク追加・編集画面は、DialogFragmentで実装されており、画面によって利用されるxmlが切り替わるようになっています。これによって、大画面の場合にはダイアログらしいUIで表示され、小さい画面では通常のFragmentのような表示がされるようになっています。

![record-211222234011.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/3ef9c868-0e51-4d99-c24a-997264665306.gif)

ダイアログらしくない画面でもDialogFragmentを利用して表現する方法は自分にはない発想で目から鱗でした。

# まとめ
大画面への対応に加えて、デザインやnavigationの利用方法についても学びのある点が多く、大画面対応をする上でどこから手をつけるか検討する際には、非常に参考になると感じました。また、アクセシビリティのサポートをはじめとして大画面対応以外にも見どころのあるアプリなので、今後の変更もチェックしていきたいと思います。