我们将通过解释 布局 和 修饰符 的基础知识 来开始我们的旅程。我们将介绍他们是如何协同工作的,Compose 提供了什么开箱即用的API,以及如何漂亮地设计您的UI
布局——因为 Compose 中的几乎所有内容都是布局
布局是Compose UI的核心组件,使您能够使用提供的各种现成API制作令人惊叹的app,并构建自己的自定义解决方案。在Compose中,您使用可组合函数来发出UI部分,但布局指定了这些元素的 精确排列和对齐。
因此,无论您使用 Compose 预置的布局还是构建您自己的自定义布局,布局都是 Compose世界的重要组成部分。 您可以将它们视为 Compose 协调器 — 指示嵌套在其中的其他可组合项的结构。 事实上,我们将在本系列的后面看到,几乎 Compose UI 中的一切都是布局! 但我们稍后会谈到这一点。
修饰符——把它们链接在一起!
现在,如果您以前使用过 Compose(如果您用过的话,对您来说是 (盲猜是技能已get的意思)!),您一定已经注意到修饰符对这个框架的重要性和关键性。 它们允许您装饰和扩充可组合项,按照您需要的方式塑造它们,并使它们能够执行您需要它们执行的操作。 通过链接你想要的任意数量的修饰符,您可以:
- 更改可组合项的大小,布局,行为 和 外观
- 添加 附加信息,如辅助功能标签
- 处理 用户输入
- 使其 可点击,可滚动,可拖动,或 可缩放
现在我们已经介绍了布局和修饰符的要点,让我们看看它们的实际应用!
更多Android学习资料 请点击免费领取
开始游戏
让我们卷起袖子,构建一个有趣的游戏画面,以了解布局和修饰符如何协同工作。 我们将从使用这两艘 Android Alien 飞船开始,逐步构建游戏的完整屏幕截图:
所以我们需要两艘飞船!可重复使用的 AndroidAlien
可组合项如下所示:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAlien(
color: Color,
modifier: Modifier = Modifier,
) {
Image(
modifier = modifier,
painter = painterResource(R.drawable.android_alien),
colorFilter = ColorFilter.tint(color = color),
contentDescription = null
)
}
复制代码
然后,调用此可组合项两次,我们将一个绿色和一个红色外星人添加到父级 AndroidAliens
中:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliens() {
AndroidAlien(
color = Color.Red,
modifier = Modifier
.size(70.dp)
.padding(4.dp)
)
AndroidAlien(
color = Color.Green,
modifier = Modifier
.size(70.dp)
.padding(4.dp)
)
}
复制代码
在构建像这样的简单可组合项时,这里有两件关于修饰符用法的事儿要解释:
- 在第一个代码段中,我们根据 Compose API 最佳实践,设置了一个带有默认参数的
modifier
参数 - 在第二个片段中,我们传递了一个由两个非常常见的修饰符组成的修饰符链来设置图像的大小 和 内边距
仅阅读最后一个代码示例,我们可能希望(并期望)Compose 以特定顺序放置这些子可组合项。 然而,当我们运行它时,我们只会在屏幕上看到一个元素:
一个落单的,孤独的 Android Alien
那艘红色飞船似乎不见了。为了调试这个问题,让我们增加红色飞船的大小:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliens() {
AndroidAlien(
color = Color.Red,
modifier = Modifier.size(100.dp)
)
// …
}
红飞船似乎有点害羞,躲在绿飞船后面。 这意味着,这两个可组合项实际上是相互重叠的,因为它们缺少关于如何布局的具体说明。 Compose 可以做很多事情,但它无法读懂你的想法! 这告诉我们,我们的飞船需要一些结构和编队,而这正是 Compose 布局所做的事情。
垂直和水平布局元素
我们不希望我们的外星飞船彼此完全重叠,而是希望它们被布置成一个挨着一个——这是一般 Android apps UI 元素的一个非常常见的用例。 我们希望我们的飞船只能够并排放置,以及一个在另一个之上:
为此,Compose 提供了 Column
和 Row
布局可组合项,分别用于垂直和水平布局元素。
让我们首先将我们的两个 AlienShips
移动到一个 Row
中,以水平排列它们:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row {
AndroidAlien(…)
AndroidAlien(…)
}
}
相反,要垂直排列 items,请将您的飞船包裹在一个Column
可组合项中:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensColumn() {
Column {
AlienShip(...)
AlienShip(...)
}
}
现在,这看起来不错,但我们想进一步调整它们的定位。我们希望它们在我们的屏幕上对齐
我们希望宇宙飞船“粘”在屏幕的特定角落,例如底部。 这意味着我们需要相应地对齐和排列这些船只,同时仍将它们保持在Column
和Row
编队中。
要在您的Column
和Row
父级中指定元素的精确定位,您可以使用Arrangement
和Alignment
。 这些属性指示布局如何定位其子项。 意思是,如果您希望嵌套的飞船“粘附”到 Column
父级的底端,请设置以下内容:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensColumn() {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
然而,当我们运行 app 时,我们仍然会看到我们之前的外星飞船:会位于屏幕的左上角位置排列
我们不能将它们粘到角落,因为像Column
和Row
这样的布局环绕着它们的子项的大小并且不会超出那个范围,所以我们看不到设置Arrangements
和Alignments
的效果。 为了让 Column
扩展到整个可用大小,我们将使用 .fillMaxSize()
修饰符,它的名称不言自明:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensColumn() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
这就是诀窍! 请注意Column
如何使用垂直排列和水平对齐,而Row
接受水平排列和垂直对齐。 这是为什么呢?! 让我们更详细地解释两者之间的区别。
Arrangement
指的是子布局在主轴上的布局方式 - Column
垂直,Row
水平。 Alignment对齐方式同理,但是是在交叉轴上 - Column
水平,Row
垂直:
有许多属性可供选择和调整您的可组合项。 你甚至可以用各种方式将你的外星人隔开,这样他们就不会彼此相处得太融洽。
打破规则
以这种方式在布局中对齐和排列组件可确保将相同的规则应用于所有子项。 但是,当我们希望第三艘流氓外星飞船脱离强加的规则时会发生什么? 让我们分解一下确切的定位——我们希望绿色和蓝色外星人遵循父级规则指令,但我们希望红色外星人反抗并“逃跑”:
为实现这一点,Compose 提供了 .align()
修饰符 用于您希望单独定位的特定子可组合项,违背了由父级强制执行的规则:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(…)
AndroidAlien(…)
AndroidAlien(…)
AndroidAlien(
color = Color.Red,
modifier = Modifier.align(Arrangement.CenterVertically)
) // 流氓叛乱飞船
}
}
让我们继续我们的游戏构建,看看当一艘大型外星母船进入竞技场时会发生什么!
顾名思义,我们希望母舰比其他外星主体大。 在这个外星人行编队中,常规船只应该占据他们需要正确渲染的最小宽度,而红色母舰应该占据该行的剩余宽度:
这是一个常见的用例,您只希望 您的一些子 UI 元素具有比其余元素更大或更小的尺寸。 您可以仅手动更改一个元素的大小,但这可能很麻烦,并且您可能希望大小相对于其余元素是相对的,而不是静态的固定值。 为此,Column
和 Row
可组合项提供 .weight()
修饰符:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(modifier = Modifier.size(70.dp)) // 正好占用 70 DP
AndroidAlien(modifier = Modifier.weight(1F)) // 占用剩余的(空间)
AndroidAlien(modifier = Modifier.size(70.dp)) // 正好占用 70 DP
}
}
母舰完全霸占了游戏空间! 不为其他元素分配权重意味着它们将根据需要仅占用宽度(在Row
中)或高度(在Column
中),剩余的宽度/高度为均匀分配到具有分配权重的元素。
但是,如果我们希望其他常规船只恰好占据宽度的 1/4 而母舰占据 2/4 的话该怎么办? 然后,我们可以通过以下方式分配权重:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(modifier = Modifier.weight(1F))
AndroidAlien(modifier = Modifier.weight(2F))
AndroidAlien(modifier = Modifier.weight(1F))
}
}
您可能会注意到常规的外星飞船现在也更大了。 分配权重的默认行为是同时调整项目的大小,以适应新分配的宽度/高度。 但是,如果您希望保持元素的原始大小,您可以将 false 传递给此修饰符的fill
参数:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
AndroidAlien(
modifier = Modifier
.size(70.dp)
.weight(1F, fill = false)
)
现在我们已经介绍了如何使用 Compose 简单但强大的列
和行
以及一些方便的修饰符来垂直地和水平地布局元素,让您的 UI 超级整洁,让我们继续!
堆叠元素
我们游戏的一个重要功能是在游戏结束 时通知您。让我们看看您需要在外星人编队的顶部显示此叠加层的设计:
你可以看到这需要在外星人上面堆叠文字,让你非常清楚地知道游戏已经结束了。 在 Compose 中,您可以使用 Box
布局作为将元素放在彼此之上或重叠它们的快速方式,并遵循可组合项的执行顺序:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensGameOverBox() {
Box {
AndroidAliensRow(…)
Text(
text = “GAME OVER”
// …
)
}
}
与我们之前的布局类似,Box
可组合项还提供了一种方法来指示它如何布置其嵌套的子项。 这里的区别在于水平和垂直之间没有区别——相反,它们合并为一个 contentAlignment
:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensGameOverBox() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
AndroidAliensRow(…)
Text(
text = “GAME OVER”
// …
)
}
}
在前面的代码片段中,我们使用 contentAlignment = Alignment.Center
将子元素居中对齐,但是您也可以使用任何现有的 2D 对齐方式。这使得 Box
布局在您希望放置嵌套的子项时非常有用,好吧,几乎在您想要的任何位置!
如果游戏确实结束了,您的主游戏屏幕可能看起来更漂亮,带有叠加的透明背景,更突显您无法再玩了
要是有一种方法可以在“GAME OVER”Text
后面设置一个灰色透明背景,然后将其展开以覆盖可用大小就好了 … 你别说还真有!
Box
布局提供了一个方便的 .matchParentSize()
修饰符,它允许子元素匹配包含的 Box
的大小:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensGameOverBox() {
Box(…) {
AndroidAliensRow(…)
Spacer(
modifier = Modifier
.matchParentSize()
.background(color = Color.Gray.copy(alpha = .7f))
)
Text(…)
}
}
但是,请记住,此子元素不参与定义Box
父级的最终大小。相反,在首先测量了所有其他子项(不使用 matchParentSize()
修饰符)以获得完整的Box的
大小后,它会匹配Box
的大小。相比之下,.fillMaxSize()
修饰符将参与定义Box
的大小,它使元素占据所有可用空间。
您可能会注意到 matchParentSize
仅在 Box
可组合项中可用。 为什么以及如何? 这让我们想到了 Compose 中一个非常重要的修饰符概念:scope safety.
修饰符作用域安全
在 Compose 中,有些修饰符只能在应用于某些可组合项的子项时才能使用。 Compose 通过自定义作用域强制执行此操作。 这就是为什么 matchParentSize
仅在 BoxScope
中可用,而 weight
仅在 ColumnScope
和 RowScope
中可用。这可以防止您添加在其他地方根本不起作用的修饰符,并节省您反复试验的时间。
插槽组件
我们的迷你游戏肯定可以在我们玩的时候,从一些关于游戏进度的信息中获益——比如一个带有当前分数的标题和一个位于底部的按钮来开始或暂停游戏,这些信息将其余的游戏内容包含在两者之间:
这可能会让您想起像顶部和底部栏这样的东西,我们的标题和按钮将放在其中。为了实现这一点,Compose 提供了一组非常方便的开箱即用的可组合项。 所以我们不得不提一下 Material components!
Jetpack Compose 提供 Material Design 的实现,这是一个用于创建数字界面的综合设计系统。 按钮、卡片、开关等材料组件以及Scaffold
等布局可用作可组合函数。 它们一起代表了用于创建用户界面的交互式构建块。
在我们添加一个标题作为顶部和一个按钮作为底部栏的例子中,Compose 和 Material 提供了Scaffold
布局,其中包含用于将各种组件布置成常见屏幕模式的插槽 . 因此,让我们将开始按钮和得分标题添加为顶部和底部栏:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
InfoHeader(…)
},
bottomBar = {
Button(…) {
Text(
text = “PRESS START”
)
}
}
) {
AndroidAliens(…)
}
}
顶部和底部栏的其他常见用例是常绿工具栏和底部导航栏。但是由于 Scaffold
可组合参数接受通用可组合 lambdas,您可以传入您想要的任何类型的可组合项:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun Scaffold(
// …
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
// …
)
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
ComposeShipsRow(…)
},
bottomBar = {
AndroidAliensRow(…)
}
) {
AndroidAliens(…)
}
}
这个开放式插槽概念称为 Slot API并且被大量使用****在 Compose 中。Scaffold
还接受 floating action button,snackbar,和许多其他自定义选项。
现在,您可能会注意到我们使用了一个 Button
可组合项作为底部栏。 这只是 Material 组件提供的许多开箱即用的解决方案中的一种。 它支持快速向屏幕添加按钮并在其中提供任何类型的内容——文本、图像和其他内容,因此也使用插槽 API 概念。
棒极了! 我们的游戏进展非常顺利。 但与我们的游戏一样,我们的 Compose 教程也会增加每个级别的难度。 那么让我们进入下一个挑战。
按需添加内容
到目前为止,我们在屏幕上以简单的 SVG 形式展示了数量非常有限的外星飞船。 但是,如果我们有数百或数千呢? 如果它们是动画的呢?
外星飞船 StackOverflow
在那种情况下,这个小小的入侵可能会导致一些严重的卡顿。 从后端一次加载您的所有内容,尤其是当它包含大数据集, 大量图像或视频时,可能会影响您应用的性能。相反,如果您可以按需加载内容,在滚动时一点一点地加载会怎么样?我个人最喜欢的场景,Compose 中的 Lazy 组件。
当 items 可见时,Lazy 列表会在屏幕上渲染一个可滚动的 items 列表,而不是一次性全部显示出来。为了构建一个令人印象深刻的绿色 Android Aliens 快速网格,我们将使用LazyVerticalGrid
可组合项,然后在其上设置固定数量的列并向其中添加 50 个 AndroidAlien
items:
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Composable
fun AndroidAliensGrid() {
// 精确地指定网格应该有 5 列
LazyVerticalGrid(columns = GridCells.Fixed(5)) {
// 添加 50 个绿色 Android 外星人 items
items(50) {
AndroidAlien(…)
}
}
}
还有许多其他 Lazy 组件可供选择:LazyHorizontalGrid
、LazyColumn
、LazyRow
以及最近的交错网格 — LazyVerticalStaggeredGrid
和 LazyHorizontalStaggeredGrid
。
Lazy 布局是一个巨大的 Compose 范畴,很容易独自成文,因此要获得关于此 API 的最佳和最详尽的信息,