1. 始める前に
Android デバイスはあらゆる形状とサイズで提供されているため、アプリのレイアウトをさまざまな画面サイズに合わせ、1 つの Android パッケージ(APK)または Android App Bundle(AAB)でさまざまなユーザーとデバイスに対応できるようにする必要があります。そのためには、特定の画面サイズやアスペクト比を前提とした固定ディメンションではなく、レスポンシブかつアダプティブなレイアウトでアプリを定義する必要があります。アダプティブ レイアウトは、使用可能な画面スペースに応じて変化します。
この Codelab では、アダプティブ UI を作成し、スポーツのリストと各スポーツの詳細情報を表示するアプリを、大画面デバイス向けに適応させる基本的な方法を説明します。このスポーツアプリは、ホーム、お気に入り、設定の 3 つの画面で構成されています。ホーム画面にはスポーツのリストが表示され、リストからスポーツを選択すると、ニュースのプレースホルダが表示されます。お気に入り画面と設定画面にもプレースホルダ テキストが表示されます。画面を切り替えるには、下部のナビゲーション メニューで関連する項目を選択します。
アプリでは、大画面で次のようなレイアウトの問題が発生します。
- 縦向きで使用できない。
- 大画面で空白スペースが多く表示される。
- 大画面で下部のナビゲーション メニューが常に表示される。
次のようにアプリの適応性を改善します。
- 横向きと縦向きの両方をサポートする。
- 水平方向のスペースが十分にある場合は、スポーツのリストと各スポーツに関するニュースを並べて表示する。
- マテリアル デザイン ガイドラインに沿ってナビゲーション コンポーネントを表示する。
このアプリは、複数のフラグメントを持つ単一アクティビティのアプリです。操作するファイルは次のとおりです。
AndroidManifest.xml
ファイル。スポーツアプリに関するメタデータを提供します。MainActivity.kt
ファイル。activity_main.xml
ファイルで生成されたコード、override
アノテーション、幅のウィンドウ クラスサイズを表すenum
クラス、アプリ ウィンドウの幅のウィンドウ クラスサイズを取得するメソッド定義が含まれています。下部のナビゲーション メニューは、アクティビティが作成されると初期化されます。activity_main.xml
ファイル。Main
アクティビティのデフォルト レイアウトを定義します。layout-sw600dp/activity_main.xml
ファイル。Main
アクティビティの代替レイアウトを定義します。代替レイアウトは、アプリ ウィンドウの幅が600dp
値以上の場合に有効となります。その内容は、最初のデフォルト レイアウトと同じです。SportsListFragment.kt
ファイル。スポーツリストの実装とカスタムの「戻る」ナビゲーションが含まれます。fragment_sports_list.xml
ファイル。スポーツリストのレイアウトを定義します。navigation_menu.xml
ファイル。下部のナビゲーション メニュー項目を定義します。
図 1. このスポーツアプリは、1 つの APK または AAB でさまざまなウィンドウ サイズをサポートします。
前提条件
- ビューベースの UI 開発に関する基本的な知識
- ラムダ関数を含む Kotlin 構文の使用経験
- Jetpack Compose の基本の Codelab を完了していること
学習内容
- 構成の変更をサポートする方法。
- コード変更の少ない代替レイアウトを追加する方法。
- ウィンドウ サイズによって異なる動作をするリストと詳細の UI を実装する方法。
作成するアプリの概要
以下をサポートする Android アプリ。
- デバイスの横向き
- タブレット、パソコン、モバイル デバイス
- 画面サイズに応じたリストと詳細の動作
必要なもの
- Android Studio Bumblebee | 2021.1.1 以上
- Android タブレットまたはエミュレータ
2. 設定する
この Codelab のコードをダウンロードして、プロジェクトを設定します。
- コマンドラインから、この GitHub リポジトリのコードのクローンを作成します。
$ git clone https://github.com/android/add-adaptive-layouts
- Android Studio で
AddingAdaptiveLayout
プロジェクトを開きます。このプロジェクトには複数の git ブランチがあります。
main
ブランチには、このプロジェクトのスターター コードが含まれています。このブランチに変更を加えて Codelab を完了します。end
ブランチには、この Codelab の解答があります。
3. トップ ナビゲーション コンポーネントを Compose に移行する
このスポーツアプリでは、下部のナビゲーション メニューをトップ ナビゲーション コンポーネントとして使用します。このナビゲーション コンポーネントは、BottomNavigationView
クラスで実装されます。このセクションでは、トップ ナビゲーション コンポーネントを Compose に移行します。
関連付けられているメニュー リソースの内容をシールクラスとして表す
navigation_menu.xml
ファイルの内容を表すシールされたMenuItem
クラスを作成し、iconId
パラメータ、labelId
パラメータ、destinationId
パラメータを渡します。- デスティネーションに対応するサブクラスとして、
Home
オブジェクト、Favorite
オブジェクト、Settings
オブジェクトを追加します。
MenuItem.kt
sealed class MenuItem(
// Resource ID of the icon for the menu item
@DrawableRes val iconId: Int,
// Resource ID of the label text for the menu item
@StringRes val labelId: Int,
// ID of a destination to navigate users
@IdRes val destinationId: Int
) {
object Home: MenuItem(
R.drawable.ic_baseline_home_24,
R.string.home,
R.id.SportsListFragment
)
object Favorites: MenuItem(
R.drawable.ic_baseline_favorite_24,
R.string.favorites,
R.id.FavoritesFragment
)
object Settings: MenuItem(
R.drawable.ic_baseline_settings_24,
R.string.settings,
R.id.SettingsFragment
)
}
下部のナビゲーション メニューをコンポーズ可能な関数として実装する
List<MenuItem>
値に設定されたmenuItems
オブジェクト、Modifier = Modifier
値に設定されたmodifier
オブジェクト、(MenuItem) -> Unit = {}
ラムダ関数に設定されたonMenuSelected
オブジェクトの 3 つのパラメータを受け取るコンポーザブル可能なBottomNavigationBar
関数を定義します。- コンポーザブル可能な
BottomNavigationBar
関数の本文で、modifier
パラメータを受け取るNavigationBar()
関数を呼び出します。 NavigationBar()
関数に渡されたラムダ関数で、menuItems
パラメータでforEach()
メソッドを呼び出してから、foreach()
メソッド呼び出しに設定されたラムダ関数でNavigationBarItem()
関数を呼び出します。NavigationBarItem()
関数に、false
値に設定されたselected
パラメータ、MenuItem
パラメータを持つonMenuSelected
関数を含むラムダ関数に設定されたonClick
パラメータ、painter = painterResource(id = menuItem.iconId)
パラメータとcontentDescription = null
パラメータを受け取るIcon
関数を含むラムダ関数に設定されたicon
パラメータ、(text = stringResource(id = menuItem.labelId)
パラメータを受け取るText
関数を含むラムダ関数に設定されたlabel
パラメータを渡します。
Navigation.kt
@Composable
fun BottomNavigationBar(
menuItems: List<MenuItem>,
modifier: Modifier = Modifier,
onMenuSelected: (MenuItem) -> Unit = {}
) {
NavigationBar(modifier = modifier) {
menuItems.forEach { menuItem ->
NavigationBarItem(
selected = false,
onClick = { onMenuSelected(menuItem) },
icon = {
Icon(
painter = painterResource(id = menuItem.iconId),
contentDescription = null)
},
label = { Text(text = stringResource(id = menuItem.labelId))}
)
}
}
}
BottomNavigationView
要素をレイアウト リソース ファイル内の ComposeView
要素に置き換える
activity_main.xml
ファイルで、BottomNavigationView
要素をComposeView
要素に置き換えます。これにより、Compose で、ビューベースの UI レイアウトにボトム ナビゲーション コンポーネントが埋め込まれます。
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/top_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Compose ベースの下部のナビゲーション メニューをビューベースの UI レイアウトと統合する
MainActivity.kt
ファイルで、MenuItem
オブジェクトのリストに設定されたnavigationMenuItems
変数を定義します。MenuItems
オブジェクトは、下部のナビゲーション メニューにリスト順に表示されます。BottomNavigationBar()
関数を呼び出して、下部のナビゲーション メニューをComposeView
オブジェクトに埋め込みます。BottomNavigationBar
関数に渡されたコールバック関数で、ユーザーが選択した項目に関連付けられたデスティネーションに移動します。
MainActivity.kt
val navigationMenuItems = listOf(
MenuItem.Home,
MenuItem.Favorites,
MenuItem.Settings
)
binding.navigation.setContent {
MaterialTheme {
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
}
}
4. 横向きをサポートする
アプリが大画面デバイスをサポートしている場合は、横向きと縦向きをサポートすることが求められます。現時点では、アプリには MainActivity
という 1 つのアクティビティのみがあります。
デバイス上のアクティビティの表示の向きは、portrait
値に設定された android:screenOrientation
属性を含む AndroidManifest.xml
ファイルで設定されます。
アプリが横向きをサポートするようにします。
android:screenOrientation
属性をfullUser
値に設定します。この設定により、ユーザーは画面の向きをロックできます。画面の向きは、デバイスの方向センサーによって 4 つの向きのいずれかに決定されます。
AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullUser">
図 2. AndroidManifest.xml
ファイルを更新すると、アプリは横向きで実行されます。
5. ウィンドウ サイズクラス
ウィンドウ サイズクラスは、アプリで使用できる未加工のウィンドウ サイズを使って、事前定義されたサイズクラス(コンパクト、中程度、拡大)にウィンドウ サイズを分類するためのブレークポイント値です。このサイズクラスは、アダプティブ レイアウトを設計、開発、テストする場合に使用します。
利用可能な幅と高さは個別に分割されるため、アプリには常に、幅のウィンドウ サイズクラスと高さのウィンドウ サイズクラスという、2 つのウィンドウ サイズクラスが関連付けられています。
図 3. 幅のウィンドウ サイズクラスと、それに関連するブレークポイント
図 4. 高さのウィンドウ サイズクラスと、それに関連するブレークポイント
ウィンドウ サイズクラスは、アプリの現在のウィンドウ サイズを表します。つまり、物理デバイスのサイズでウィンドウ サイズクラスを判断することはできません。アプリが同じデバイスで実行されている場合でも、関連するウィンドウ サイズクラスは、アプリを分割画面モードで実行する場合など、構成によって異なります。その結果、次の 2 点が重要となります。
- 物理デバイスで、特定のウィンドウ サイズクラスが保証されるわけではありません。
- ウィンドウ サイズクラスは、アプリの全期間を通じて変わる可能性があります。
アプリにアダプティブ レイアウトを追加したら、すべてのウィンドウ サイズ(特にコンパクト、中程度、拡大のウィンドウ サイズクラス)でアプリをテストします。ウィンドウ サイズクラスごとにテストが必要ですが、多くの場合、十分ではありません。UI を正しくスケーリングできるように、アプリをさまざまなウィンドウ サイズでテストすることが重要です。詳しくは、大画面のレイアウトと大画面のアプリの品質をご覧ください。
6. 大画面でリストペインと詳細ペインを並べて配置する
リストと詳細の UI では、現在の幅のウィンドウ サイズクラスによって異なる動作が必要になる場合があります。中程度の幅または拡大幅のウィンドウ サイズクラスがアプリに関連付けられている場合、アプリには、リストペインと詳細ペインを並べて表示するのに十分なスペースがあり、ユーザーは画面遷移なしで項目のリストと選択した項目の詳細情報を確認できます。しかし、小さな画面では表示が過密になる場合があるため、1 つずつペインを表示することをおすすめします(最初はリストペインを表示)。ユーザーがリスト内の項目をタップすると、詳細ペインに選択した項目の詳細が表示されます。SlidingPaneLayout
クラスは、これら 2 つのユーザー エクスペリエンスのどちらが現在のウィンドウ サイズに適しているかを判断するロジックを管理します。
リストペインのレイアウトを設定する
SlidingPaneLayout
クラスはビューベースの UI コンポーネントです。リストペインのレイアウト リソース ファイル(この Codelab では fragment_sports_list.xml
ファイル)を変更します。
SlidingPaneLayout
クラスは 2 つの子要素を受け取ります。各子要素の幅と重みの属性は、ウィンドウが両方のビューを並べて表示するのに十分な大きさであるかどうかを判断するための SlidingPaneLayout
クラスの重要な要素です。ウィンドウが十分な大きさでない場合は、全画面表示のリストが全画面表示の詳細 UI に置き換えられます。ウィンドウ サイズがペインを並べて表示するための最小要件よりも大きい場合、2 つのペインのサイズを比率で指定するために weight 値が参照されます。
SlidingPaneLayout
クラスが fragment_sports_list.xml
ファイルに適用されました。リストペインの幅は 1280dp
になるように構成されています。そのため、リストペインと詳細が並んで表示されません。
画面の幅が 580dp
より大きい場合、ペインを並べて表示します。
RecyclerView
を280dp
の幅に設定します。
fragment_sports_list.xml
<androidx.slidingpanelayout.widget.SlidingPaneLayout
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"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SportsListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<androidx.fragment.app.FragmentContainerView
android:layout_height="match_parent"
android:layout_width="300dp"
android:layout_weight="1"
android:id="@+id/detail_container"
android:name="com.example.android.sports.NewsDetailsFragment"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
図 5. レイアウト リソース ファイルを更新すると、リストペインと詳細ペインが並んで表示されます。
詳細ペインを入れ替える
中程度の幅または拡大幅のウィンドウ サイズクラスをアプリに関連付けると、リストペインと詳細ペインが並んで表示されるようになりました。ただし、ユーザーがリストペインから項目を選択すると、画面は詳細ペインに完全に遷移します。
図 6. リストからスポーツを選択すると、画面が詳細ペインに遷移します。
この問題は、ユーザーがリストペインから項目を選択したときにトリガーされるナビゲーションが原因で発生します。関連するコードは SportsListFragment.kt
ファイルにあります。
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
リストペインと詳細ペインを並べて表示するための十分なスペースがない場合にのみ、画面が詳細ペインに完全に遷移するようにします。
adapter
関数変数に、SlidingPaneLayout
クラスのisSlidable
属性が true であり、SlidingPaneLayout
クラスのisOpen
属性が false であるかどうかをチェックするif
ステートメントを追加します。
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
if(slidingPaneLayout.isSlidable && !slidingPaneLayout.isOpen){
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
}
7. 幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択する
マテリアル デザインでは、アプリは適応的にコンポーネントを選択する必要があります。このセクションでは、現在の幅のウィンドウ サイズクラスに基づいて、上部のナビゲーション バーのナビゲーション コンポーネントを選択します。次の表に、各ウィンドウ サイズクラスで想定されるナビゲーション コンポーネントを示します。
幅のウィンドウ サイズクラス | ナビゲーション コンポーネント |
コンパクト | ボトム ナビゲーション |
中程度 | ナビゲーション レール |
拡大 | 固定的なナビゲーション ドロワー |
ナビゲーション レールを実装する
List<MenuItem>
値に設定されたmenuItems
オブジェクト、Modifier
値に設定されたmodifier
オブジェクト、onMenuSelected
ラムダ関数の 3 つのパラメータを受け取るコンポーズ可能なNavRail()
関数を作成します。- 関数の本文で、
modifier
オブジェクトをパラメータとして受け取るNavigationRail()
関数を呼び出します。 BottomNavigationBar()
関数内のNavigationBarItem
関数の場合と同様に、menuItems
オブジェクト内の各MenuItem
オブジェクトに対してNavigationRailItem()
関数を呼び出します。
固定的なナビゲーション ドロワーを実装する
List<MenuItem>
値に設定されたmenuItems
オブジェクト、Modifier
値に設定されたmodifier
オブジェクト、onMenuSelected
ラムダ関数の 3 つのパラメータを受け取るコンポーズ可能なNavigationDrawer()
関数を作成します。- 関数の本文で、
modifier
オブジェクトをパラメータとして受け取るColumn()
関数を呼び出します。 menuItems
オブジェクト内の各MenuItem
オブジェクトに対してRow()
関数を呼び出します。BottomNavigationBar()
関数内のNavigationBarItem
関数の場合と同様に、Row()
関数の本文で、icon
ラベルとtext
ラベルを追加します。
幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択する
- 現在の幅のウィンドウ サイズクラスを取得するには、
ComposeView
オブジェクトのsetContent()
メソッドに渡されたコンポーズ可能な関数内でrememberWidthSizeClass()
関数を呼び出します。 - 条件付きブランチを作成して、取得した幅のウィンドウ サイズクラスに基づいてナビゲーション コンポーネントを選択し、選択したコンポーネントを呼び出します。
Modifier
オブジェクトをNavigationDrawer
関数に渡して、その幅を256dp
値として指定します。
ActivityMain.kt
binding.navigation.setContent {
MaterialTheme {
when(rememberWidthSizeClass()){
WidthSizeClass.COMPACT ->
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.MEDIUM ->
NavRail(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.EXPANDED ->
NavigationDrawer(
menuItems = navigationMenuItems,
modifier = Modifier.width(256.dp)
) { menuItem ->
navController.navigate(screen.destinationId)
}
}
}
}
ナビゲーション コンポーネントを適切な位置に配置する
これで、アプリは現在のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択できるようになりましたが、選択したコンポーネントは期待どおりに配置できません。これは、ComposeView
要素が FragmentViewContainer
要素の下に配置されているためです。
MainActivity
クラスの代替レイアウト リソースを更新します。
layout-sw600dp/activity_main.xml
ファイルを開きます。ComposeView
要素とFragmentContainer
要素を水平方向に配置するように制約を更新します。
layout-sw600dp/activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintLeft_toRightOf="@+id/top_navigation"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/top_navigation"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
変更後、アプリは、関連付けられた幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択します。1 つ目のスクリーンショットは、中程度の幅のウィンドウ サイズクラスの画面を示しています。2 つ目のスクリーンショットは、拡大幅のウィンドウ サイズクラスの画面を示しています。
図 7. 中程度の幅および拡大幅のウィンドウ クラスの画面
8. 完了
これで、この Codelab は終了です。Compose を使用してビューベースの Android アプリにアダプティブ レイアウトを追加する方法を学習しました。その過程で、SlidingPaneLayout
クラス、ウィンドウ サイズクラス、幅のウィンドウ サイズクラスに基づくナビゲーション コンポーネントの選択について学習しました。