1. 简介
作为一种 UI 工具包,Compose 使实现应用程序的设计变得容易。您描述了希望 UI 的外观,Compose 负责在屏幕上绘制它。本 Codelab 教您如何编写 Compose UI。它假设您了解 基础知识 Codelab 中教授的概念,因此请确保首先完成该 Codelab。在基础知识 Codelab 中,您学习了如何使用 Surfaces
、Rows
和 Columns
实现简单的布局。您还使用 padding
、fillMaxWidth
和 size
等修饰符增强了这些布局。
在本 Codelab 中,您将实现一个更逼真且复杂的布局,并在此过程中了解各种开箱即用的可组合项和修饰符。完成本 Codelab 后,您应该能够将基本应用程序的设计转换为可工作的代码。
本 Codelab 不会向应用程序添加任何实际行为。要改为了解状态和交互,请完成 Compose 中的状态 Codelab。
在您完成本 Codelab 的过程中,以下代码随附内容可为您提供更多支持
您将学到什么
在本 Codelab 中,您将学习
- 修饰符如何帮助您增强可组合项。
- Column 和 LazyRow 等标准布局组件如何定位子可组合项。
- 对齐方式和排列方式如何更改子可组合项在其父级中的位置。
- Scaffold 和 Bottom Navigation 等 Material 可组合项如何帮助您创建全面的布局。
- 如何使用插槽 API 构建灵活的可组合项。
- 如何为不同的屏幕配置构建布局。
您需要什么
- 最新版 Android Studio.
- Kotlin 语法的使用经验,包括 Lambda 表达式。
- Compose 的基本使用经验。如果您尚未完成,请在开始本 Codelab 之前完成 Jetpack Compose 基础知识 Codelab。
- 可组合项和修饰符的基本概念。
您将构建什么
在本 Codelab 中,您将根据设计师提供的模型实现逼真的应用程序设计。MySoothe 是一款健康应用程序,列出了各种改善身心健康的方法。它包含一个列出您收藏夹的区域和一个包含健身锻炼的区域。应用程序如下所示
2. 设置
在此步骤中,您将下载包含主题和一些基本设置的代码。
获取代码
本 Codelab 的代码可在 codelab-android-compose GitHub 代码库 中找到。要克隆它,请运行
$ git clone https://github.com/android/codelab-android-compose
或者,您可以下载两个 zip 文件
查看代码
下载的代码包含所有可用 Compose Codelab 的代码。要完成本 Codelab,请在 Android Studio 中打开 BasicLayoutsCodelab
项目。
建议您从 main
分支中的代码开始,并按照 Codelab 的步骤逐步完成,按照自己的节奏进行。
3. 从计划开始
我们将从实现应用程序的纵向设计开始 - 让我们仔细看看
当您被要求实现某个设计时,一个好的开始方法是清楚地了解其结构。不要立即开始编写代码,而是分析设计本身。如何将此 UI拆分为多个可重用的部分?
因此,让我们对我们的设计进行尝试。在最高抽象级别,我们可以将此设计分解为两部分
- 屏幕内容。
- 底部导航。
深入分析,屏幕内容包含三个子部分
- 搜索栏。
- 一个名为“调整您的身体”的部分。
- 一个名为“收藏夹”的部分。
在每个部分中,您还可以看到一些可重复使用的较低级别组件
- 水平滚动行中显示的“调整您的身体”元素。
- 水平滚动网格中显示的“收藏夹”卡片。
现在您已经分析了设计,您可以开始为 UI 的每个已识别部分实现可组合项。从最低级别的可组合项开始,然后将其组合成更复杂的项。在本 Codelab 结束时,您的新应用程序将看起来像提供的模型。
4. 搜索栏 - 修饰符
要转换为可组合项的第一个元素是搜索栏。让我们再看一下设计
仅根据此屏幕截图,以像素完美的方式实现此设计将非常困难。通常,设计师会传达更多关于设计的信息。他们可以授予您访问其设计工具的权限,或共享所谓的红线设计。在本例中,我们的设计师交付了红线设计,您可以使用它来读取任何尺寸值。设计显示带有 8dp 网格叠加,因此您可以轻松地看到元素之间和周围有多少空间。此外,还明确添加了一些间距以阐明某些尺寸。
您可以看到搜索栏的高度应为 56 个密度无关像素。它还应填充其父级的全部宽度。
要实现搜索栏,请使用称为 文本字段 的 Material 组件。Compose Material 库包含一个名为 TextField
的可组合项,它是此 Material 组件的实现。
从基本的 TextField
实现开始。在您的代码库中,打开 MainActivity.kt
并搜索 SearchBar
可组合项。
在名为 SearchBar
的可组合项内部,编写基本的 TextField
实现
import androidx.compose.material3.TextField
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
需要注意的一些事项
- 您硬编码了文本字段的值,并且
onValueChange
回调没有任何作用。由于这是一个专注于布局的 Codelab,因此您将忽略与状态相关的任何内容。
- 可组合函数
SearchBar
接受一个modifier
参数并将其传递给TextField
。根据 Compose 指南,这是一个最佳实践。这允许方法的调用者修改可组合项的外观和行为,从而使其更灵活且更易于重用。在本 Codelab 中,您将继续使用此最佳实践来处理所有可组合项。
让我们看看此可组合项的预览。请记住,您可以使用 Android Studio 中的 预览功能 快速迭代各个可组合项。MainActivity.kt
包含您将在本 Codelab 中构建的所有可组合项的预览。在本例中,方法 SearchBarPreview
呈现我们的 SearchBar
可组合项,并带有一些背景和填充以提供更多上下文。使用您刚刚添加的实现,它应如下所示
缺少一些内容。首先,让我们使用 修饰符 修复可组合项的大小。
编写可组合项时,您可以使用修饰符来
- 更改可组合项的大小、布局、行为和外观。
- 添加信息,例如辅助功能标签。
- 处理用户输入。
- 添加高级交互,例如使元素可点击、可滚动、可拖动或可缩放。
您调用的每个可组合项都有一个 modifier
参数,您可以将其设置为调整该可组合项的外观、感觉和行为。设置修饰符时,您可以链接多个修饰符方法以创建更复杂的调整。
在本例中,搜索栏的高度应至少为 56dp,并填充其父级的宽度。要查找此处的正确修饰符,您可以查看 修饰符列表 并查看 大小部分。对于高度,您可以使用 heightIn
修饰符。这确保可组合项具有特定的最小高度。但是,当例如用户增大系统字体大小时,它可能会变得更大。对于宽度,您可以使用 fillMaxWidth
修饰符。此修饰符确保搜索栏占用其父级的全部水平空间。
更新修饰符以匹配以下代码
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.TextField
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
)
}
在本例中,因为一个修饰符影响宽度,另一个影响高度,所以这些修饰符的顺序无关紧要。
您还必须设置 TextField
的一些参数。尝试通过设置参数值使可组合项看起来像设计。以下是作为参考的设计
以下是要采取的更新实现步骤
- 添加搜索图标。
TextField
包含一个参数leadingIcon
,它接受另一个可组合项。在其中,您可以设置一个Icon
,在本例中应该是Search
图标。确保使用正确的 ComposeIcon
导入。 - 您可以使用
TextFieldDefaults.textFieldColors
覆盖特定颜色。将文本字段的focusedContainerColor
和unfocusedContainerColor
设置为 MaterialTheme 的surface
颜色。 - 添加占位符文本“Search”(您可以在字符串资源
R.string.placeholder_search
中找到它)。
完成后,您的可组合项应类似于此
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedContainerColor = MaterialTheme.colorScheme.surface
),
placeholder = {
Text(stringResource(R.string.placeholder_search))
},
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
)
}
请注意
- 您添加了一个显示搜索图标的
leadingIcon
。此图标不需要内容描述,因为文本字段的占位符已描述了文本字段的含义。请记住,内容描述通常用于辅助功能目的,并为应用的用户提供图像或图标的文本表示。
- 要调整文本字段的背景颜色,您需要设置
colors
属性。可组合项包含一个组合参数,而不是每个颜色都有一个单独的参数。在这里,您传递TextFieldDefaults
数据类的副本,其中您只更新不同的颜色。在本例中,只有unfocusedContainerColor
和focusedContainerColor
颜色。
在此步骤中,您了解了如何使用可组合参数和修饰符来更改可组合项的外观和感觉。这适用于 Compose 和 Material 库提供的可组合项,也适用于您自己编写的可组合项。您应该始终考虑提供参数来自定义您正在编写的可组合项。您还应该添加一个 modifier
属性,以便可以从外部调整可组合项的外观和感觉。
5. 对齐您的身体 - 对齐方式
您将实现的下一个可组合项是“对齐您的身体”元素。让我们看看它的设计,包括它旁边的红线设计
红线设计现在还包含基于基线的间距。以下是我们从中获得的信息
- 图像的高度应为 88dp。
- 文本基线与图像之间的间距应为 24dp。
- 基线与元素底部之间的间距应为 8dp。
- 文本应具有 bodyMedium 的排版样式。
要实现此可组合项,您需要一个 Image
和一个 Text
可组合项。它们需要包含在 Column
中,以便它们彼此垂直放置。
在您的代码中找到 AlignYourBodyElement
可组合项,并使用此基本实现更新其内容
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null
)
Text(text = stringResource(R.string.ab1_inversions))
}
}
请注意
- 您将图像的
contentDescription
设置为 null,因为此图像纯粹是装饰性的。图像下方的文本描述了足够的信息,因此图像不需要额外的描述。 - 您正在使用硬编码的图像和文本。在下一步中,您将将其移动到使用
AlignYourBodyElement
可组合项中提供的参数,以使其成为动态的。
查看此可组合项的预览
有一些改进需要进行。最明显的是,图像太大,形状也不像圆形。您可以使用 size
和 clip
修饰符以及 contentScale
参数来调整 Image
可组合项。
与您在上一步中看到的 fillMaxWidth
和 heightIn
修饰符类似,size
修饰符会调整可组合项以适应特定大小。 clip
修饰符的工作方式不同,它**会调整可组合项的外观**。您可以将其设置为任何 Shape
,它会将可组合项的内容剪辑到该形状。
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text(text = stringResource(R.string.ab1_inversions))
}
}
目前,您在预览中的设计如下所示
图像也需要正确缩放。为此,我们可以使用 Image
的 contentScale
参数。有几种选择,最值得注意的是
在本例中,裁剪类型是正确的选择。应用修饰符和参数后,您的代码应如下所示
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text( text = stringResource(R.string.ab1_inversions) )
}
}
您的设计现在应如下所示
下一步,通过设置 Column
的对齐方式来水平对齐文本。
通常,要在父容器中对齐可组合项,您需要设置该父容器的**对齐方式**。因此,您不是告诉子项在其父项中定位自身,而是告诉父项如何对齐其子项。
对于 Column
,您决定如何水平对齐其子项。选项包括
- 开始
- 水平居中
- 结束
对于 Row
,您需要设置垂直对齐方式。选项与 Column
的选项类似
- 顶部
- 垂直居中
- 底部
对于 Box
,您需要组合水平和垂直对齐方式。选项包括
- 左上
- 上中
- 右上
- 中左
- 中心
- 中右
- 左下
- 下中
- 右下
所有容器的子项都将遵循相同的对齐模式。您可以通过向其中添加 align
修饰符来覆盖单个子项的行为。
对于此设计,文本应水平居中。为此,请将 Column
的 horizontalAlignment
设置为水平居中
import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Image(
//..
)
Text(
//..
)
}
}
在实现这些部分后,只需要进行一些细微的更改即可使可组合项与设计完全相同。尝试自己实现这些更改,或者如果遇到困难,请参考最终代码。请考虑以下步骤
- 使图像和文本动态化。将它们作为参数传递给可组合函数。不要忘记更新相应的预览并传入一些硬编码数据。
- 更新文本以使用 bodyMedium 排版样式。
- 根据图表更新文本元素的基线间距。
完成这些步骤的实现后,您的代码应类似于此
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
@DrawableRes drawable: Int,
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text(
text = stringResource(text),
modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
MySootheTheme {
AlignYourBodyElement(
text = R.string.ab1_inversions,
drawable = R.drawable.ab1_inversions,
modifier = Modifier.padding(8.dp)
)
}
}
在“设计”选项卡中查看 AlignYourBodyElement。
6. 收藏夹卡片 - 材料表面
下一个要实现的可组合项在某种程度上类似于“对齐身体”元素。这是设计,包括红线
在本例中,提供了可组合项的完整大小。您可以看到文本应为 titleMedium。
此容器使用 surfaceVariant 作为其背景颜色,这与整个屏幕的背景不同。它还有圆角。我们使用 Material 的 Surface
可组合项为收藏夹卡片指定这些属性。
您可以通过设置其参数和修饰符来根据您的需要调整 Surface
。在本例中,表面应具有圆角。您可以为此使用 shape
参数。与上一步中图像的 Shape
设置不同,您将使用来自 Material 主题的值。
让我们看看它是什么样子
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
modifier = modifier
) {
Row {
Image(
painter = painterResource(R.drawable.fc2_nature_meditations),
contentDescription = null
)
Text(text = stringResource(R.string.fc2_nature_meditations))
}
}
}
让我们看看此实现的预览
接下来,应用上一步中吸取的经验教训。
- 设置
Row
的宽度,并垂直对齐其子项。 - 根据图表设置图像的大小,并在其容器中裁剪它。
在查看解决方案代码之前,尝试自己实现这些更改!
您的代码现在应如下所示
import androidx.compose.foundation.layout.width
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(255.dp)
) {
Image(
painter = painterResource(R.drawable.fc2_nature_meditations),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(80.dp)
)
Text(
text = stringResource(R.string.fc2_nature_meditations)
)
}
}
}
预览现在应如下所示
要完成此可组合项,请执行以下步骤
- 使图像和文本动态化。将它们作为参数传递给可组合函数。
- 将颜色更新为 surfaceVariant。
- 更新文本以使用 titleMedium 排版样式。
- 更新图像和文本之间的间距。
您的最终结果应类似于此
@Composable
fun FavoriteCollectionCard(
@DrawableRes drawable: Int,
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(255.dp)
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(80.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
//..
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
MySootheTheme {
FavoriteCollectionCard(
text = R.string.fc2_nature_meditations,
drawable = R.drawable.fc2_nature_meditations,
modifier = Modifier.padding(8.dp)
)
}
}
查看 FavoriteCollectionCardPreview 的预览。
7. 对齐您的身体行 - 布局
现在您已创建了屏幕上显示的基本可组合项,您可以开始创建屏幕的不同部分。
从“对齐您的身体”可滚动行开始。
这是此组件的红线设计
请记住,网格中的一个块代表 8dp。因此,在此设计中,第一个项目之前和一行中最后一个项目之后有 16dp 的间距。每个项目之间有 8dp 的间距。
在 Compose 中,您可以使用 LazyRow
可组合项来实现这样的可滚动行。有关列表的 文档 包含更多关于 Lazy 列表的信息,例如 LazyRow
和 LazyColumn
。对于此 Codelab,您只需知道 LazyRow
只渲染屏幕上显示的元素,而不是同时渲染所有元素,这有助于保持应用的性能。
从 LazyRow
的基本实现开始。
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
如您所见,LazyRow
的子项不是可组合项。相反,您使用 Lazy 列表 DSL,它提供诸如 item
和 items
之类的方法,这些方法将可组合项作为列表项发出。对于提供的 alignYourBodyData
中的每个项目,您都会发出之前实现的 AlignYourBodyElement
可组合项。
注意它是如何显示的。
我们在线框图设计中看到的间距仍然缺失。要实现这些间距,您需要了解 **排列**。
在上一步骤中,您了解了对齐方式,对齐方式用于在 **横轴** 上对齐容器的子项。对于 Column
,横轴是水平轴,而对于 Row
,横轴是垂直轴。
但是,我们也可以决定如何在容器的 **主轴** 上放置子可组合项(对于 Row
为水平,对于 Column
为垂直)。
对于 Row
,您可以选择以下排列方式:
对于 Column
则:
除了这些排列方式之外,您还可以使用 Arrangement.spacedBy()
方法在每个子可组合项之间添加固定间距。
在本例中,您需要使用 spacedBy
方法,因为您希望在 LazyRow
中的每个项目之间放置 8dp 的间距。
import androidx.compose.foundation.layout.Arrangement
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
现在设计如下所示:
您还需要在 LazyRow
的两侧添加一些填充。在这种情况下,添加简单的填充修饰符将不起作用。尝试向 LazyRow
添加填充,并使用 交互式预览 查看它的行为。
如您所见,当滚动时,第一个和最后一个可见项目在屏幕的两侧都被截断了。
为了保持相同的填充,但仍然在父列表的边界内滚动内容而不会裁剪它,所有列表都为 LazyRow
提供了一个名为 contentPadding
的参数,并将其设置为 16.dp
。
import androidx.compose.foundation.layout.PaddingValues
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
尝试使用交互式预览查看填充带来的差异。
8. 收藏夹网格 - Lazy 网格
接下来要实现的部分是屏幕的“收藏夹收藏”部分。此可组合项需要一个网格,而不是单行。
您可以通过创建 LazyRow
并让每个项目包含两个 FavoriteCollectionCard
实例的 Column
来实现此部分,类似于上一部分。但是,在此步骤中,您将使用 LazyHorizontalGrid
,它提供了从项目到网格元素的更友好的映射。
从具有两行的简单网格实现开始。
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = modifier
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(item.drawable, item.text)
}
}
}
如您所见,您只需将上一步骤中的 LazyRow
替换为 LazyHorizontalGrid
。但是,这还不会为您提供正确的结果。
网格占据与其父容器一样多的空间,这意味着收藏夹收藏卡在垂直方向上被拉伸得太多。
调整可组合项,以便:
- 网格具有 16.dp 的水平内容填充。
- 水平和垂直排列方式的间距为 16.dp。
- 网格的高度为 168.dp。
- FavoriteCollectionCard 的修饰符指定高度为 80.dp。
最终代码应如下所示:
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.height(168.dp)
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
}
}
}
预览应如下所示:
9. 首页部分 - 插槽 API
在 MySoothe 主屏幕上,有多个遵循相同模式的 **部分**。它们都有一个标题,一些内容根据部分而有所不同。这是我们要实现的线框图设计:
如您所见,每个部分都有一个 **标题** 和一个 **插槽**。标题有一些相关的间距和样式信息。插槽可以根据部分动态填充不同的内容。
要实现此灵活的部分容器,您可以使用所谓的 *插槽 API*。在实现之前,请阅读文档页面上有关 基于插槽的布局 的部分。这将帮助您了解什么是基于插槽的布局以及如何使用插槽 API 来构建此类布局。
调整 HomeSection
可组合项以接收标题和插槽内容。您还应该调整关联的预览以使用“调整您的身体”标题和内容调用此 HomeSection
。
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(stringResource(title))
content()
}
}
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
MySootheTheme {
HomeSection(R.string.align_your_body) {
AlignYourBodyRow()
}
}
}
您可以将 content
参数用于可组合项的插槽。这样,当您使用 HomeSection
可组合项时,您可以使用尾随 lambda 来填充内容插槽。当可组合项提供多个插槽来填充时,您可以为它们提供有意义的名称,以表示它们在更大的可组合容器中的功能。例如,Material 的 TopAppBar
提供了 title
、navigationIcon
和 actions
的插槽。
让我们看看使用此实现部分的外观:
Text 可组合项需要更多信息才能使其与设计保持一致。
更新它,以便:
- 它使用 titleMedium 排版。
- 文本基线与顶部的间距为 40dp。
- 基线与元素底部的间距为 16dp。
- 水平填充为 16dp。
您的最终解决方案应如下所示:
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.paddingFromBaseline(top = 40.dp, bottom = 16.dp)
.padding(horizontal = 16.dp)
)
content()
}
}
10. 首页 - 滚动
现在您已创建了所有单独的构建块,您可以将它们组合成完整的屏幕实现。
这是您尝试实现的设计:
我们只是将搜索栏和下面的两个部分彼此放置。您需要添加一些间距以使所有内容都符合设计。我们之前未使用过的一个可组合项是 Spacer
,它可以帮助我们在 Column
内部放置额外的空间。如果您改为设置 Column
的填充,您将获得之前在收藏夹收藏网格中看到的相同截断行为。
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(modifier) {
Spacer(Modifier.height(16.dp))
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = R.string.align_your_body) {
AlignYourBodyRow()
}
HomeSection(title = R.string.favorite_collections) {
FavoriteCollectionsGrid()
}
Spacer(Modifier.height(16.dp))
}
}
虽然设计在大多数设备尺寸上都非常适合,但如果设备高度不够(例如在横向模式下),则需要使其能够垂直滚动。这要求您添加滚动行为。
如前所述,LazyRow
和 LazyHorizontalGrid
等 Lazy 布局会自动添加滚动行为。但是,您并不总是需要 Lazy 布局。一般来说,**当您在列表中有很多元素或要加载的大型数据集时,您会使用 Lazy 布局**,因此一次发出所有项目会带来性能成本并会降低应用速度。当列表只有有限数量的元素时,您可以选择使用简单的 Column
或 Row
并 **手动添加滚动行为**。为此,您可以使用 verticalScroll
或 horizontalScroll
修饰符。这些需要一个 ScrollState
,其中包含滚动的当前状态,用于从外部修改滚动状态。在这种情况下,您不希望修改滚动状态,因此您只需使用 rememberScrollState
创建一个持久性的 ScrollState
实例。
您的最终结果应如下所示:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(16.dp))
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = R.string.align_your_body) {
AlignYourBodyRow()
}
HomeSection(title = R.string.favorite_collections) {
FavoriteCollectionsGrid()
}
Spacer(Modifier.height(16.dp))
}
}
要验证可组合项的滚动行为,请限制预览的高度并在 交互式预览 中运行它。
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
MySootheTheme { HomeScreen() }
}
11. 底部导航 - Material
现在您已实现了屏幕的内容,您就可以添加窗口装饰了。对于 MySoothe,有一个导航栏允许用户在不同的屏幕之间切换。
首先,实现导航栏可组合项,然后将其包含在您的应用中。
让我们看一下设计:
值得庆幸的是,您不必自己从头开始实现整个可组合项。您可以使用 NavigationBar
可组合项,它是 Compose Material 库的一部分。在 NavigationBar
可组合项内部,您可以添加一个或多个 NavigationBarItem
元素,然后 Material 库将自动对其进行样式设置。
从底部导航的基本实现开始
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
NavigationBar(
modifier = modifier
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(
text = stringResource(R.string.bottom_navigation_home)
)
},
selected = true,
onClick = {}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(
text = stringResource(R.string.bottom_navigation_profile)
)
},
selected = false,
onClick = {}
)
}
}
这是基本实现的样子 - 内容颜色和导航栏的颜色之间没有太多对比。
您应该进行一些样式调整。首先,您可以通过设置其containerColor
参数来更新底部导航的背景颜色。您可以为此使用 Material 主题中的 surfaceVariant 颜色。您的最终解决方案应如下所示
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
modifier = modifier
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
现在导航栏应该如下所示,请注意它提供了更多对比度。
12. MySoothe 应用 - 脚手架
在此步骤中,创建全屏实现,包括底部导航。使用 Material 的Scaffold
可组合项。Scaffold
为实现 Material 设计的应用提供了一个**顶级可配置的可组合项**。它包含各种 Material 概念的插槽,其中之一是底部栏。在此底部栏中,您可以放置您在上一步中创建的底部导航可组合项。
实现MySootheAppPortrait()
可组合项。这是您应用的顶级可组合项,因此您应该
- 应用
MySootheTheme
Material 主题。 - 添加
Scaffold
。 - 将底部栏设置为您的
SootheBottomNavigation
可组合项。 - 将内容设置为您的
HomeScreen
可组合项。
您的最终结果应为
import androidx.compose.material3.Scaffold
@Composable
fun MySootheAppPortrait() {
MySootheTheme {
Scaffold(
bottomBar = { SootheBottomNavigation() }
) { padding ->
HomeScreen(Modifier.padding(padding))
}
}
}
您的实现现已完成!如果您想检查您的版本是否以像素完美的方式实现,您可以将此图像与您自己的预览实现进行比较。
13. 导航栏 - Material
在为应用创建布局时,您还需要注意其在多种配置(包括手机上的横向模式)下的外观。这是应用在横向模式下的设计,请注意底部导航如何变成屏幕内容左侧的栏。
要实现此功能,您将使用NavigationRail
可组合项,它是 Compose Material 库的一部分,并且具有与用于创建底部导航栏的NavigationBar
类似的实现。在 NavigationRail 可组合项内部,您将添加NavigationRailItem
元素以用于主页和个人资料。
让我们从导航栏的基本实现开始。
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
NavigationRail(
) {
Column(
) {
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
}
您应该进行一些样式调整。
- 在栏的开头和结尾添加 8.dp 的填充。
- 通过使用 Material 主题中的背景颜色设置其
containerColor
参数来更新导航栏的背景颜色。通过设置背景颜色,图标和文本的颜色会自动适应主题的onBackground
颜色。 - 该列应填充最大高度。
- 将列的垂直排列设置为居中。
- 将列的水平对齐方式设置为水平居中。
- 在两个图标之间添加 8.dp 的填充。
您的最终解决方案应如下所示:
import androidx.compose.foundation.layout.fillMaxHeight
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
NavigationRail(
modifier = modifier.padding(start = 8.dp, end = 8.dp),
containerColor = MaterialTheme.colorScheme.background,
) {
Column(
modifier = modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
Spacer(modifier = Modifier.height(8.dp))
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
}
现在,让我们将导航栏添加到横向布局中。
对于应用的纵向版本,您使用了 Scaffold。但是,对于横向版本,您将使用 Row 并将导航栏和屏幕内容并排放置。
@Composable
fun MySootheAppLandscape() {
MySootheTheme {
Row {
SootheNavigationRail()
HomeScreen()
}
}
}
当您在纵向版本中使用 Scaffold 时,它还会负责为您设置内容颜色为背景。要设置导航栏的颜色,请将 Row 包裹在 Surface 中并将其设置为背景颜色。
@Composable
fun MySootheAppLandscape() {
MySootheTheme {
Surface(color = MaterialTheme.colorScheme.background) {
Row {
SootheNavigationRail()
HomeScreen()
}
}
}
}
14. MySoothe 应用 - 窗口大小
您的横向模式预览看起来很棒。但是,如果您在设备或模拟器上运行应用并将其转向侧面,它将不会显示横向版本。这是因为我们需要告诉应用何时显示应用的哪种配置。为此,请使用calculateWindowSizeClass()
函数查看手机处于何种配置。
窗口大小类宽度有三种:紧凑型、中型和扩展型。当应用处于纵向模式时,它是紧凑型宽度,当它处于横向模式时,它是扩展型宽度。出于本 Codelab 的目的,您将不使用中型宽度。
在 MySootheApp 可组合项中,将其更新为接收设备的WindowSizeClass。如果它是紧凑型,则传入应用的纵向版本。如果是横向,则传入应用的横向版本。
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
MySootheAppPortrait()
}
WindowWidthSizeClass.Expanded -> {
MySootheAppLandscape()
}
}
}
在setContent()
中,创建一个名为 windowSizeClass 的 val,将其设置为calculateWindowSize()
并将其传递给 MySootheApp()。
由于calculateWindowSize()
仍处于实验阶段,因此您需要选择加入ExperimentalMaterial3WindowSizeClassApi
类。
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
MySootheApp(windowSizeClass)
}
}
}
现在 - 在您的模拟器或设备上运行应用,并观察显示在旋转时的变化。
15. 祝贺
恭喜,您已成功完成本 Codelab 并学习了更多关于 Compose 中布局的知识。通过实现一个真实世界的设计,您学习了修饰符、对齐方式、排列、Lazy 布局、插槽 API、滚动、Material 组件和特定于布局的设计。
查看 Compose 学习路径上的其他 Codelab。并查看代码示例。
文档
有关这些主题的更多信息和指导,请查看以下文档