1. 简介
在 Android 平台上开发应用程序的一大优势是能够接触到各种形态因素的用户,例如可穿戴设备、折叠屏手机、平板电脑、桌面电脑,甚至电视。当用户使用应用程序时,他们可能希望在大屏幕设备上使用同一应用程序以利用更大的屏幕空间。越来越多的 Android 用户在各种屏幕尺寸的多个设备上使用他们的应用程序,并期望在所有设备上都能获得高质量的用户体验。
到目前为止,您已经学习了如何主要为移动设备创建应用程序。在本 Codelab 中,您将学习如何转换您的应用程序以使其适应其他屏幕尺寸。您将使用自适应导航布局模式,这些模式对于移动设备和大屏幕设备(如折叠屏手机、平板电脑和桌面电脑)来说都美观且易用。
先决条件
- 熟悉 Kotlin 编程,包括类、函数和条件语句
- 熟悉使用
ViewModel
类 - 熟悉创建
Composable
函数 - 有使用 Jetpack Compose 构建布局的经验
- 有在设备或模拟器上运行应用程序的经验
您将学到什么
- 如何为简单的应用程序在屏幕之间创建导航,而无需使用 Navigation Graph
- 如何使用 Jetpack Compose 创建自适应导航布局
- 如何创建自定义后退处理程序
您将构建什么
- 您将在现有的 Reply 应用程序中实现动态导航,以使其布局适应所有屏幕尺寸
最终产品将如下面的图片所示
您需要什么
- 一台具有互联网访问权限的电脑、一个网络浏览器和 Android Studio
- 访问 GitHub
2. 应用概述
Reply 应用简介
Reply 应用是一个多屏幕应用,类似于 电子邮件客户端。
它包含 4 个不同的类别,通过不同的选项卡显示,分别是:收件箱、已发送、草稿和垃圾邮件。
下载初始代码
在 Android Studio 中,打开 basic-android-kotlin-compose-training-reply-app
文件夹。
3. 初始代码演练
Reply 应用中的重要目录
Reply 应用项目的數據和 UI 层被分隔到不同的目录中。 ReplyViewModel
、ReplyUiState
和其他可组合项位于 ui
目录中。定义数据层和数据提供程序类的 data
和 enum
类位于 data
目录中。
Reply 应用中的数据初始化
Reply 应用通过 ReplyViewModel
中的 initializeUIState()
方法初始化数据,该方法在 init
函数中执行。
ReplyViewModel.kt
...
init {
initializeUIState()
}
private fun initializeUIState() {
var mailboxes: Map<MailboxType, List<Email>> =
LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
_uiState.value = ReplyUiState(
mailboxes = mailboxes,
currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
?: LocalEmailsDataProvider.defaultEmail
)
}
...
屏幕级可组合项
与其他应用程序一样,Reply 应用使用 ReplyApp
可组合项作为主可组合项,其中声明了 viewModel
和 uiState
。各种 viewModel()
函数也作为 lambda 参数传递给 ReplyHomeScreen
可组合项。
ReplyApp.kt
...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
ReplyHomeScreen(
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
}
其他可组合项
ReplyHomeScreen.kt
:包含主屏幕的屏幕可组合项,包括导航元素。ReplyHomeContent.kt
:包含定义主屏幕更详细的可组合项的可组合项。ReplyDetailsScreen.kt
:包含详细信息屏幕的屏幕可组合项和较小的可组合项。
请随时详细浏览每个文件,以在继续 Codelab 的下一部分之前更好地了解可组合项。
4. 在没有导航图的情况下更改屏幕
在之前的路径中,您学习了如何使用 NavHostController
类在一个屏幕之间导航到另一个屏幕。使用 Compose,您还可以通过利用运行时可变状态,使用简单的条件语句更改屏幕。这在像 Reply 应用这样的小型应用程序中特别有用,在这些应用程序中,您只需要在两个屏幕之间切换。
使用状态更改更改屏幕
在 Compose 中,当状态发生更改时,屏幕会重新组合。您可以使用简单的条件语句来响应状态更改,从而更改屏幕。
您将使用条件语句在用户位于主屏幕时显示主屏幕的内容,在用户不位于主屏幕时显示详细信息屏幕。
通过完成以下步骤修改 Reply 应用以允许在状态更改时更改屏幕
- 在 Android Studio 中打开初始代码。
- 在
ReplyHomeScreen.kt
中的ReplyHomeScreen
可组合项中,使用if
语句将ReplyAppContent
可组合项包装起来,以用于replyUiState
对象的isShowingHomepage
属性为true
的情况。
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Int) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
您现在必须考虑用户不在主屏幕时的情况,方法是显示详细信息屏幕。
- 添加一个
else
分支,并在其主体中使用ReplyDetailsScreen
可组合项。将replyUIState
、onDetailScreenBackPressed
和modifier
作为参数添加到ReplyDetailsScreen
可组合项中。
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Int) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
replyUiState
对象是一个状态对象。因此,当 replyUiState
对象的 isShowingHomepage
属性发生变化时,ReplyHomeScreen
可组合项会重新组合,并且 if/else
语句在运行时重新评估。这种方法支持在不同屏幕之间导航,而无需使用 NavHostController
类。
创建自定义后退处理程序
使用 NavHost
可组合项在屏幕之间切换的一个优点是,先前屏幕的方向保存在后退栈中。这些保存的屏幕允许系统后退按钮在调用时轻松导航到上一个屏幕。由于 Reply 应用不使用 NavHost
,因此您必须手动添加代码来处理后退按钮。您将在下一步中执行此操作。
完成以下步骤,在 Reply 应用中创建自定义后退处理程序
- 在
ReplyDetailsScreen
可组合项的第一行,添加一个BackHandler
可组合项。 - 在
BackHandler
可组合项的主体中调用onBackPressed()
函数。
ReplyDetailsScreen.kt
...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
BackHandler {
onBackPressed()
}
...
5. 在大屏幕设备上运行应用
使用可调整大小的模拟器检查您的应用
为了创建可用的应用,开发人员需要了解用户在各种形态因素下的体验。因此,您必须从开发过程的开始就测试各种形态因素下的应用。
您可以使用许多不同屏幕尺寸的模拟器来实现此目标。但是,这样做可能很麻烦,尤其是在您同时为多个屏幕尺寸构建时。您可能还需要测试正在运行的应用程序如何响应屏幕尺寸更改,例如方向更改、桌面上的窗口尺寸更改以及折叠屏手机上的折叠状态更改。
Android Studio 通过引入 **可调整大小的模拟器** 来帮助您测试这些场景。
完成以下步骤以设置可调整大小的模拟器
- 在 Android Studio 中,选择 **工具** > **设备管理器**。
- 在 **设备管理器** 中,单击 **+** 图标以创建虚拟设备。
- 选择 **手机** 类别和 **可调整大小(实验性)** 设备。
- 单击 **下一步**。
- 选择 **API 级别 34** 或更高版本。
- 单击 **下一步**。
- 命名您的新 Android 虚拟设备。
- 单击 **完成**。
在大屏幕模拟器上运行应用
现在您已经设置了可调整大小的模拟器,让我们看看应用程序在大屏幕上的显示效果。
- 在可调整大小的模拟器上运行应用程序。
- 为显示模式选择 **平板电脑**。
- 检查平板电脑模式下的横向模式下的应用程序。
请注意,平板电脑屏幕显示是水平拉长的。虽然这种方向在功能上可以工作,但它可能不是大屏幕空间的最佳利用方式。让我们在下一步中解决这个问题。
为大屏幕设计
当您在平板电脑上查看此应用程序时,您首先想到的可能是它设计糟糕且不吸引人。您是对的:此布局**并非**为大屏幕使用而设计。
在为大屏幕(如平板电脑和折叠屏手机)设计时,您必须考虑用户工效学以及用户手指与屏幕的距离。对于移动设备,用户的手指可以轻松触及大部分屏幕;交互式元素(如按钮和导航元素)的位置并不那么关键。但是,对于大屏幕,将关键的交互式元素放在屏幕中间可能会使它们难以触及。
如您在 Reply 应用中看到的,为大屏幕设计不仅仅是拉伸或放大 UI 元素以适应屏幕。它是一个机会,可以使用增加的屏幕空间为您的用户创造不同的体验。例如,您可以在同一屏幕上添加另一个布局,以避免需要导航到另一个屏幕或使多任务处理成为可能。
这种设计可以提高用户的工作效率并促进更大的参与度。但在部署此设计之前,您必须首先学习如何为不同的屏幕尺寸创建不同的布局。
6. 使您的布局适应不同的屏幕尺寸
什么是断点?
您可能想知道如何为同一个应用程序显示不同的布局。简而言之,就是根据不同的状态使用条件语句,就像您在本 Codelab 的开头所做的那样。
要创建自适应应用程序,您需要布局根据屏幕尺寸更改。布局更改的测量点称为断点。Material Design 创建了一个 有见地的断点范围,涵盖大多数 Android 屏幕。
此断点范围表显示,例如,如果您的应用程序当前正在屏幕尺寸小于 600 dp 的设备上运行,则应显示移动布局。
使用窗口大小类
为 Compose 引入的 WindowSizeClass
API 使 Material Design 断点的实现更加简单。
窗口大小类为宽度和高度引入了三种尺寸类别:紧凑、中等和扩展。
完成以下步骤,在 Reply 应用中实现 WindowSizeClass
API
- 将
material3-window-size-class
依赖项添加到模块build.gradle.kts
文件中。
build.gradle.kts
...
dependencies {
...
implementation("androidx.compose.material3:material3-window-size-class")
...
- 添加依赖项后,单击 **立即同步** 以同步 Gradle。
在更新了build.gradle.kts
文件后,您现在可以创建一个变量,用于在任何给定时间存储应用窗口的大小。
- 在
MainActivity.kt
文件中的onCreate()
函数中,将calculateWindowSizeClass()
方法(传入参数this
上下文)赋值给名为windowSize
的变量。 - 导入相应的
calculateWindowSizeClass
包。
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val layoutDirection = LocalLayoutDirection.current
Surface (
// ...
) {
val windowSize = calculateWindowSizeClass(this)
ReplyApp()
...
- 注意
calculateWindowSizeClass
语法的红色下划线,它显示了红色灯泡。点击windowSize
变量左侧的红色灯泡,然后选择**在“onCreate”上选择“ExperimentalMaterial3WindowSizeClassApi”**,以在onCreate()
方法顶部创建一个注解。
您可以在 MainActivity.kt
中使用 WindowWidthSizeClass
变量来确定在各种可组合项中显示哪个布局。让我们准备 ReplyApp
可组合项以接收此值。
- 在
ReplyApp.kt
文件中,修改ReplyApp
可组合项以接受WindowWidthSizeClass
作为参数,并导入相应的包。
ReplyApp.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
...
- 在
MainActivity.kt
文件的onCreate()
方法中,将windowSize
变量传递给ReplyApp
组件。
MainActivity.kt
...
setContent {
ReplyTheme {
Surface {
val windowSize = calculateWindowSizeClass(this)
ReplyApp(
windowSize = windowSize.widthSizeClass
)
...
您还需要更新应用的预览,以便包含 windowSize
参数。
- 将
WindowWidthSizeClass.Compact
作为windowSize
参数传递给预览组件的ReplyApp
可组合项,并导入相应的包。
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Preview(showBackground = true)
@Composable
fun ReplyAppCompactPreview() {
ReplyTheme {
Surface {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact,
)
}
}
}
- 要根据屏幕大小更改应用布局,请根据
WindowWidthSizeClass
值在ReplyApp
可组合项中添加一个when
语句。
ReplyApp.kt
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
when (windowSize) {
WindowWidthSizeClass.Compact -> {
}
WindowWidthSizeClass.Medium -> {
}
WindowWidthSizeClass.Expanded -> {
}
else -> {
}
}
...
至此,您已建立了一个基础,可以使用 WindowSizeClass
值来更改应用中的布局。下一步是确定您希望应用在不同屏幕尺寸上如何显示。
7. 实现自适应导航布局
实现自适应 UI 导航
当前,底部导航 用于所有屏幕尺寸。
如前所述,此导航元素并不理想,因为用户可能会发现难以在较大的屏幕上触达这些基本导航元素。幸运的是,响应式 UI 的导航 中为各种窗口大小类推荐了不同的导航元素模式。对于回复应用,您可以实现以下元素
导航栏 是 Material Design 提供的另一个导航组件,它允许从应用侧面访问主要目的地的紧凑导航选项。
类似地,持久/永久导航抽屉 由 Material Design 创建,作为另一个选项,为较大的屏幕提供符合人体工程学的访问方式。
实现导航抽屉
要为扩展屏幕创建导航抽屉,您可以使用 navigationType
参数。请完成以下步骤以执行此操作
- 为了表示不同类型的导航元素,请在新的包
utils
中创建一个新文件WindowStateUtils.kt
,该包位于ui
目录下。 - 添加一个
Enum
类来表示不同类型的导航元素。
WindowStateUtils.kt
package com.example.reply.ui.utils
enum class ReplyNavigationType {
BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}
要成功实现导航抽屉,您需要根据应用的窗口大小确定导航类型。
- 在
ReplyApp
可组合项中,创建一个navigationType
变量,并根据when
语句中的屏幕大小为其分配相应的ReplyNavigationType
值。
ReplyApp.kt
...
import com.example.reply.ui.utils.ReplyNavigationType
...
val navigationType: ReplyNavigationType
when (windowSize) {
WindowWidthSizeClass.Compact -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
WindowWidthSizeClass.Medium -> {
navigationType = ReplyNavigationType.NAVIGATION_RAIL
}
WindowWidthSizeClass.Expanded -> {
navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
}
else -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
}
...
您可以在 ReplyHomeScreen
可组合项中使用 navigationType
值。您可以通过将其设为可组合项的参数来为此做好准备。
- 在
ReplyHomeScreen
可组合项中,添加navigationType
作为参数。
ReplyHomeScreen.kt
...
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
)
...
- 将
navigationType
传递到ReplyHomeScreen
可组合项。
ReplyApp.kt
...
ReplyHomeScreen(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
...
接下来,您可以创建一个分支,以便在用户在扩展屏幕上打开应用并显示主屏幕时,使用导航抽屉显示应用内容。
- 在
ReplyHomeScreen
可组合项主体中,为navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage
条件添加一个if
语句。
ReplyHomeScreen.kt
import androidx.compose.material3.PermanentNavigationDrawer
...
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
}
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
...
- 要创建永久抽屉,请在 if 语句的主体中创建
PermanentNavigationDrawer
可组合项,并将NavigationDrawerContent
可组合项作为drawerContent
参数的输入。 - 将
ReplyAppContent
可组合项作为PermanentNavigationDrawer
的最终 lambda 参数。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
...
- 添加一个
else
分支,该分支使用以前的可组合项主体来维护非扩展屏幕的先前分支。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
}
...
- 在**平板电脑**模式下运行应用。您应该会看到以下屏幕
实现导航栏
与导航抽屉实现类似,您需要使用 navigationType
参数在导航元素之间切换。
首先,让我们为中等屏幕添加一个导航栏。
- 首先准备
ReplyAppContent
可组合项,方法是添加navigationType
作为参数。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- 将
navigationType
值传递到两个ReplyAppContent
可组合项。
ReplyHomeScreen.kt
...
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
...
接下来,让我们添加分支,以便应用能够在某些情况下显示导航栏。
- 在
ReplyAppContent
可组合项主体的第一行,将ReplyNavigationRail
可组合项包装在AnimatedVisibility
可组合项中,并将visible
参数设置为true
(如果ReplyNavigationType
值为NAVIGATION_RAIL
)。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(
MaterialTheme.colorScheme.inverseOnSurface
)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
...
- 要正确对齐可组合项,请将
AnimatedVisibility
可组合项和ReplyAppContent
主体中找到的Column
可组合项都包装在Row
可组合项中。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
...
最后,让我们确保在某些情况下显示底部导航。
- 在
ReplyListOnlyContent
可组合项之后,将ReplyBottomNavigationBar
可组合项包装在AnimatedVisibility
可组合项中。 - 当
ReplyNavigationType
值为BOTTOM_NAVIGATION
时,设置visible
参数。
ReplyHomeScreen.kt
...
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
...
- 在**展开式折叠**模式下运行应用。您应该会看到以下屏幕
8. 获取解决方案代码
要下载完成的 codelab 的代码,您可以使用以下 git 命令
git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git cd basic-android-kotlin-compose-training-reply-app git checkout nav-update
或者,您可以将存储库下载为 zip 文件,将其解压缩,然后在 Android Studio 中打开它。
如果您想查看解决方案代码,请在 GitHub 上查看。
9. 结论
恭喜!您距离使 Reply 应用能够适应所有屏幕尺寸又近了一步,方法是实现了自适应导航布局。您使用许多 Android 外形规格增强了用户体验。在下一个 codelab 中,您将通过实现自适应内容布局、测试和预览进一步提高使用自适应应用的技能。
不要忘记在社交媒体上使用 #AndroidBasics 分享您的作品!