1. 简介
在 Android 平台中开发应用的一大优势是,您有很大的机会吸引不同类型的设备(例如穿戴式设备、可折叠设备、平板电脑、桌面设备,甚至是电视)上的用户。在使用某款应用时,用户可能也会希望在大屏设备上使用该应用,以充分利用更大的屏幕。Android 用户越来越多地在屏幕尺寸各异的多种设备上使用应用,并且希望在所有设备上都能获得优质的用户体验。
到目前为止,您已经了解了如何打造主要适合移动设备的应用。在此 Codelab 中,您将学习如何促进应用转型,使其能够适应其他屏幕尺寸。您将使用自适应导航布局模式,这类模式非常美观,同时适用于移动设备和大屏设备,例如可折叠设备、平板电脑和桌面设备。
前提条件
- 熟悉 Kotlin 编程,包括类、函数和条件
- 熟悉如何使用
ViewModel
类 - 熟悉如何创建
Composables
函数 - 拥有使用 Jetpack Compose 构建布局的经验
- 拥有在设备或模拟器上运行应用的经验
学习内容
- 如何在没有导航图的情况下为简单的应用创建屏幕之间的导航
- 如何使用 Jetpack Compose 创建自适应导航布局
- 如何创建自定义返回处理程序
构建内容
- 您将在现有的 Reply 应用中实现动态导航栏,使其布局能够适应所有屏幕尺寸
完成后的效果将如下图所示:
所需条件
- 可连接到互联网的计算机、网络浏览器和 Android Studio
- 能够访问 GitHub
2. 应用概览
Reply 应用简介
Reply 是一款多屏幕应用,类似于电子邮件客户端。
该应用包含 4 个不同类别,分别显示在不同的标签页中,即“Inbox”“Sent”“Drafts”和“Spam”。
下载起始代码
在 Android Studio 中,打开 basic-android-kotlin-compose-training-reply-app
文件夹。
- 进入为此项目提供的 GitHub 代码库页面。
- 验证分支名称是否与此 Codelab 中指定的分支名称一致。例如,在以下屏幕截图中,分支名称为 main。
- 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。
- 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open。
注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。
- 在文件浏览器中,转到解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用按预期构建。
3. 起始代码演示
Reply 应用中的重要目录
Reply 应用项目的数据和界面层分到了不同的目录中。ReplyViewModel
、ReplyUiState
和其他可组合项位于 ui
目录中。用于定义数据层的 data
和 enum
类以及数据提供程序类均位于 data
目录中。
Reply 应用中的数据初始化
Reply 应用通过 ReplyViewModel
中的 initilizeUIState()
方法初始化数据,该方法在 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
函数也会作为 ReplyHomeScreen
可组合项的 lambda 参数进行传递。
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
可组合项中,针对replyUiState
对象的isShowingHomepage
属性为true
的情况,使用if
语句封装ReplyAppContent
可组合项。
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
)
}
}
现在,您必须通过显示详情屏幕,将用户不在主屏幕上的情况纳入考虑。
- 添加一个正文中包含
ReplyDetailsScreen
可组合项的else
分支。将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,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
BackHandler {
onBackPressed()
}
...
5. 在大屏设备上运行应用
使用可调整大小的模拟器来检查应用
若要打造易于使用的应用,开发者需要了解其用户在各类设备上的体验。因此,从开发流程之初,您就必须针对各类设备测试应用。
您可以使用屏幕尺寸各异的多个模拟器来实现此目标。不过,这样做可能会很麻烦,尤其是在同时针对多种屏幕尺寸进行构建时。您可能还需要测试正在运行的应用会如何响应屏幕尺寸的变化,例如屏幕方向变化、桌面设备中的窗口大小变化以及可折叠设备上的折叠状态变化。
Android Studio 中推出了可调整大小的模拟器,可帮助您测试这些情形。
完成以下步骤,以便设置可调整大小的模拟器:
- 确保您运行的是 Android Studio Chipmunk | 2021.2.1 或更高版本。
- 在 Android Studio 中,依次选择 Tools > Device Manager。
- 在 Device Manager 中,点击 Create device。
- 依次选择 Phone 类别和 Resizable (Experimental) 设备。
- 点击 Next。
- 选择 API Level 33。
- 点击 Next。
- 为新的 Android 虚拟设备命名。
- 点击 Finish。
在大屏模拟器上运行应用
现在,您已经设置了可调整大小的模拟器,接下来我们来看看应用在大屏幕上的呈现效果。
- 在可调整大小的模拟器上运行应用。
- 选择 Tablet 作为显示模式。
- 在“Tablet”模式下,以横屏模式查看应用。
请注意,该应用在平板电脑屏幕上以水平拉伸状态显示。尽管这种屏幕方向在功能上没问题,但可能无法充分利用大屏幕的屏幕空间。下面我们来解决这个问题。
专为大屏幕而设计
当看到这款应用在平板电脑上的呈现效果时,您的第一感觉是,这款应用设计不合理,不太有吸引力。这完全正确:此布局不适用于大屏幕。
在针对大屏幕(例如平板电脑和可折叠设备)进行设计时,您必须考虑用户工效学以及用户的手指与屏幕之间的距离。使用移动设备时,用户的手指可以轻松地触及大部分屏幕;互动元素(例如按钮和导航元素)的位置则不那么重要。但是,对于大屏幕,将关键的互动元素放在屏幕中间可能会使其难以触及。
正如您在 Reply 应用中看到的那样,针对大屏幕设计不仅仅是拉伸或放大界面元素,使其适配屏幕。这是一次机会,让您能够利用较大的屏幕空间来为用户打造不同的体验。例如,您可以在同一屏幕上添加其他布局,让用户无需导航到其他屏幕,或者实现多任务处理功能。
这种设计可以提高用户的工作效率,提升互动度。不过,在部署此设计之前,您必须先了解如何针对不同的屏幕尺寸创建不同的布局。
6. 让布局适应不同的屏幕尺寸
什么是断点?
您可能想知道如何为同一应用显示不同的布局。简单点回答就是,针对不同的状态使用条件,就像您在此 Codelab 开头时所做的那样。
如要构建自适应应用,您需要根据屏幕尺寸更改布局。布局发生更改的测量点称为“断点”。Material Design 创建了一个可以涵盖大多数 Android 屏幕的主观断点范围。
根据此断点范围表格,假设您的应用目前在屏幕尺寸小于 600 dp 的设备上运行,则应显示移动设备布局。
使用窗口大小类别
为 Compose 引入的 WindowSizeClass
API 简化了 Material Design 断点的实现。
窗口大小类别针对宽度和高度引入了三种尺寸:较小、中等和较大。
完成以下步骤,在 Reply 应用中实现 WindowSizeClass
API:
- 将
material3-window-size-class
依赖项添加到模块build.gradle
文件中。
build.gradle
...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
- 添加依赖项后,点击 Sync Now 以同步 Gradle。
确保 build.grade
文件最新后,您现在可以创建一个变量,以便在任意给定时间存储应用窗口的大小。
- 在
MainActivity.kt
文件的onCreate()
函数中,将参数中传入this
上下文的calculateWindowSizeClass
方法分配给名为windowSize
的变量。 - 导入相应的
calculateWindowSizeClass
软件包。
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val windowSize = calculateWindowSizeClass(this)
ReplyApp()
...
- 可以注意到
calculateWindowSizeClass
语法有一条红色下划线,显示了红色灯泡。点击windowSize
变量左侧的红色灯泡,然后选择 Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate',以在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
) {
...
- 将
windowSize
变量传递给MainActivity.kt
文件的onCreate()
方法中的ReplyApp
组件。
MainActivity.kt
...
setContent {
ReplyTheme {
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 ReplyAppPreview() {
ReplyTheme {
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. 实现自适应导航布局
实现自适应界面导航
目前,底部导航栏适用于所有屏幕尺寸。
如前所述,此导航元素设计不太合理,因为用户会发现在较大的屏幕上很难触及这些基本的导航元素。幸运的是,在响应式界面的导航中针对不同的窗口大小类别,提供了不同的导航元素模式建议。对于 Reply 应用,您可以实现以下元素:
侧边导航栏是 Material Design 推出的又一个导航组件,它支持用于从应用的一侧访问主要目标页面的较小导航选项。
同样,持续存在的/永久性抽屉式导航栏由 Material Design 构建,是针对较大屏幕提供工效学访问设计的另一种选择。
实现抽屉式导航栏
如要为较大的屏幕创建抽屉式导航栏,您可以使用 navigationType
参数。为此,请完成以下步骤:
- 为了表示不同类型的导航元素,请在
ui
目录下的新软件包utils
中创建一个新文件WindowStateUtils.kt
。 - 添加一个
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
...
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
...
@OptIn(ExperimentalMaterial3Api::class)
@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
的 final lambda 参数。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
...
- 添加一个
else
分支,该分支使用之前的可组合项正文,以针对较大屏幕以外的屏幕维护之前的分支。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
) {
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
)
}
}
}
...
- 向
ReplyHomeScreen
可组合项添加实验性注解。您需要执行此操作,因为PermanentNavigationDrawer
API 仍处于实验阶段。
ReplyHomeScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Email) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
) {
...
- 在 Tablet 模式下运行应用。您会看到以下屏幕:
实现侧边导航栏
与抽屉式导航栏实现类似,您需要使用 navigationType
参数在导航元素之间切换。
我们先来为中等屏幕添加一个侧边导航栏。
- 首先,通过将
navigationType
添加为参数,准备ReplyAppContent
可组合项。
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
可组合项中,并在ReplyNavigationType
值为NavigationRail
时将visibility
参数设为true
。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit) = {},
onEmailCardPressed: (Email) -> Unit = {},
navigationItemContentList: List<NavigationItemContent>,
modifier: 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)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
...
- 若要正确对齐可组合项,请将
ReplyAppContent
正文中的AnimatedVisibility
可组合项和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.fillMaxSize()) {
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)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
}
...
最后,我们来确保在某些情况下显示底部导航栏。
- 在
ReplyListOnlyContent
可组合项之后,使用AnimatedVisibility
可组合项封装ReplyBottomNavigationBar
可组合项。 - 当
ReplyNavigationType
的值为BOTTOM_NAVIGATION
时,设置visible
参数。
ReplyHomeScreen.kt
...
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
...
- 在 Unfolded foldable 模式下运行应用。您应该会看到以下屏幕:
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 标签在社交媒体上分享您的作品!