この記事はAndroid Advent Calendar 2021 22日目の記事です。
Android12Lもベータがリリースされ、タブレットや折りたたみ式デバイスなど大画面対応の機運も高まってきているように感じます。
この記事では、そんな大画面対応をGoogleが公開しているTrackrアプリでどのように対応・実装されているのかについて紹介していきます。
Trackrについて
Trackrはサンプルのタスク管理アプリで、新規タスクの追加や編集、タスクのアーカイブ、タスクのお気に入りといった定番の機能を持っています。モダンな技術を利用しつつ、アクセシビリティの観点からも実装が行われている他、Composeでの実装パターンも開発が進んでいます。
今回はその中で大画面対応にスポットを当てて紹介します。
Navigation Rail
Navigation Railは、3~7画面のナビゲーションを設けることのできるコンポーネントで、Bottom Navigationが画面下に配置されるのに対して、画面横に配置されるため大きな画面サイズでもスペースを有効活用できます。
実装
Trackrアプリでは、レイアウトリソースを分けることで、w600dp以上の画面の場合にはNavigation Railが利用され、それ以下の場合には、Bottom App Barが利用されるようになっています。
Bottom App Bar | Navigation Rail |
---|---|
Bottom Navigationと同様に app:menu
でアイテムを設定し、app:labelVidibilityMode
でラベルの表示有無を設定します。
<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で実装する場合と近いものになっていると思います。
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の言語の場合には右側に配置されるため、それぞれで個別に調整が行われています。
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
SlidingPaneLayoutは大画面では2ペインのレイアウト、小さい画面では1ペインのみのレイアウトのように自動的に切り替えを行うコンポーネントです。
SlidingPaneLayoutでは、layout_width
の合計を上回る画面幅を持つ端末で表示された場合には、並べて表示されます。画面に入りきらない場合には、スライドアニメーションとともに画面が切り替わるような挙動になります。
実装
Trackrの場合には、@dimen/list_pane_width
は360dp、@detail_pane_width
は400dpが設定されていることから、760dp以上の横幅の場合、並べて表示される挙動となります。
<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が設定されており、以下のように通知を受け取って選択したタスクが切り替わるようになっています。
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
タスク追加・編集画面は、DialogFragmentで実装されており、画面によって利用されるxmlが切り替わるようになっています。これによって、大画面の場合にはダイアログらしいUIで表示され、小さい画面では通常のFragmentのような表示がされるようになっています。
ダイアログらしくない画面でもDialogFragmentを利用して表現する方法は自分にはない発想で目から鱗でした。
まとめ
大画面への対応に加えて、デザインやnavigationの利用方法についても学びのある点が多く、大画面対応をする上でどこから手をつけるか検討する際には、非常に参考になると感じました。また、アクセシビリティのサポートをはじめとして大画面対応以外にも見どころのあるアプリなので、今後の変更もチェックしていきたいと思います。