Layouts和Modifiers是compose UI的核心组件,使您能够使用提供的各种即用型api制作令人惊叹的功能性应用程序。
我们来使用它们构建一个名为Android外星人的迷你像素化游戏:

1、准备知识
在compose中,您使用可组合函数来发出UI的一部分,但layout是指定这些元素的精确排列和对齐。您可以将它们视为组合协调器,规定嵌套在其中的其他可组合项的结构,
scss
Layout {
Image(...)
Layout {
Text(...)
Text(...)
}
}


结构如下:

您一定注意到modifier对于这个框架来说是多么重要和关键,它们允许装饰和增强可组合项按照您需要的方式塑造它们,并通过链接任意数量的modifier使它们能够执行您需要的的操作。
scss
Layout {
Image(
...
Modifier
.clip(CircleShape)
.size(40.dp)
)
Layout {
Text(
...
Modifier.padding(top = 4.dp)
)
Text(...)
}
}

您可以更改可组合项的大小、布局行为和外观等其他信息,例如contentDescription = "Image"
、clickable { ... }
、可滚动、可拖动或可缩放等。
2、小步实践
接下来我们撸起袖子构建一个有趣的游戏屏幕来了解布局修改器如何协同工作。
我们将从使用Android外星飞船开始,并逐渐构成可重复使用的游戏屏幕。

像这样,我们只是加载图像资源
kotlin
@Composable
fun AndroidAlien(
color: Color,
modifier: Modifier = Modifier
) {
Image(
modifier = modifier,
painter = painterResource(R.drawable.android_alien),
colorFilter = ColorFilter.tint(color = color)
// ...
)
}
然后调用次可组合项两次,我们向父级Android外星人添加红色和绿色外星人,这里有两点是修饰符,我们正在构建这样的简单可组合项。
首先我们设置根据组合API最佳实践使用默认参数,第二我们传递一个非常常见的修饰符转成的修饰符链来设置图像的大小和填充。
ini
@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)
)
}
但是当我们运行时,我们只在屏幕上看到一个元素,红色的似乎缺少调试。
kotlin
class MainActivity: ComponentActivity() {
// ...
setContent {
// ...
AndroidAliens()
}
}

让我们增加红色大小,现在红色躲在绿色,这两个可组合项实际上是相互重叠的,
ini
@Composable
fun AndroidAliens() {
AndroidAlien(
color = Color.Red,
modifier = Modifier
.size(100.dp)
.padding(4.dp)
)
AndroidAlien(
color = Color.Green,
modifier = Modifier
.size(70.dp)
.padding(4.dp)
)
}

因为他们缺少有关如何布局的具体说明。希望它们彼此相邻防止,这是UI元素和Android应用程序一个非常常见的用例。
一般来说,我们希望我们的飞船能够被定为并排以及一个在另一个之上

现在要进行此组合,提供Row和Column,分别用于水平和垂直布局元素。

3、Layout
首先将两艘外星飞船移入一行,将它们以水平形式布局
kotlin
@Composable
fun AndroidAliens() {
Row {
AndroidAlien(
color = Color.Green,
// ...
)
AndroidAlien(
color = Color.Red,
// ...
)
}
}

将它们以垂直形式布局
kotlin
@Composable
fun AndroidAliens() {
Column {
AndroidAlien(
color = Color.Green,
// ...
)
AndroidAlien(
color = Color.Red,
// ...
)
}
}

现在看起来不错,但我们想稍微调整他们的位置,我们希望宇宙飞船对齐并粘合到屏幕的特定点,例如我们案例中的顶部。这意味着我们相应地赌气和安排这些船只,同时仍将它们保持在行和列形式以指定这些布局中元素的精确定位,您可以使用arrangements
和alignments
属性只是布局在屏幕上的确切位置来定位其子项,这意味着如果您想要将子布局放置腹部局的顶部中心,您设置一下内容。
scss
@Composable
fun AndroidAliensRow() {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(...)
AndroidAlien(...)
}
}
@Composable
fun AndroidAliensColumn() {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AndroidAlien(...)
AndroidAlien(...)
}
}

但是当我们运行应用程序时,我们仍然可以看到我们的外星飞船,但是它们并没有水平和垂直居中,因为Column和Row这样的布局会围绕子级的大小,并且它们不会超出这个范围,因此我们现在无法看到设置的arrangement和alignment。

以解决这个问题,我们需要在整个可用大小上扩展布局的问题,因此我们将使用fillMaxSize,填充最大大小修改器。
scss
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
AndroidAlien(...)
AndroidAlien(...)
}
}
@Composable
fun AndroidAliensColumn() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
AndroidAlien(...)
AndroidAlien(...)
}
}

Arrangement - main axis

Alignment - croww axis

对于Row和Column,还有许多对齐和排列的属性可供使用:


scss
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
AndroidAlien(...)
AndroidAlien(...)
AndroidAlien(...)
AndroidAlien(...)
}
}

上面这个布局是相同的规则应用于所有子项,但是当我们想要其中一个子项区别于其他子项规则

scss
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
AndroidAlien(...)
AndroidAlien(...)
AndroidAlien(...)
AndroidAlien(
modifier = Modifier.align(Arrangement.CenterVertically
) // Rogue Rebellion ship
}
}
为了实现词组和,提供了对齐修饰符,用于您希望单独定位的特定子可组合项定义父级强制执行的规则让我们继续构建游戏。
看看当一个大的子可组合项出现时会发生什么?

如上图所示,常规的船应该占据正确渲染所需的最小宽度,而母舰应该只占据剩余的宽度,您可以手动更改一个大小,但我们希望大小相对于其余元素,而不是该列的静态固定值,并且可组合项提供权重修改器。
ini
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(modifier = Modifier.size(70.dp))
AndroidAlien(modifier = Modifier.weight(1F))
AndroidAlien(modifier = Modifier.size(70.dp))
}
}
但是如果我们希望其他常规传播恰好占据四分之一的空间怎么办?母舰占据四分之二

ini
@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(modifier = Modifier.weight(1F)
AndroidAlien(modifier = Modifier.weight(2F)
AndroidAlien(modifier = Modifier.weight(1F)
}
}
我们游戏的一个重要功能是让您知道游戏何时结束并在顶部显示"GAME OVER"。在Compose中,您可以使用Box布局作为一种快速方法,将元素彼此放在一起或按照可组合项的执行顺序重叠它们,与我们之前的Layout类似,Box还提供了一种准确指示的方法,在哪里布局嵌套子元素。这里的区别在于,水平和垂直没有区别。
kotlin
@Composable
fun AndroidAliensGameOverBox() {
Box {
AndroidAliensRow(...)
Text(
text = "GAME OVER"
// ...
)
}
}

下面我们将子元素内容据居中对齐。
kotlin
@Composable
fun AndroidAliensGameOverBox() {
Box (
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
AndroidAliensRow(...)
Text(
text = "GAME OVER"
// ...
)
}
}
Alignment还可以使用下面任何一种对齐方式,当您希望将嵌套子项放置在任何您想要的位置。

如果游戏确实位于主游戏屏幕上,那么使用覆盖的透明北京可能看起来会更好,使其更加明显您根本无法再玩。使用一个透明的Spacer扩展到整个Box布局提供了一个方便的的matchParentSize()
,允许子元素匹配Box的已定义大小。但请记住,但是该元素不占用定义box父级的最终尺寸的一部分,它只是匹配它。相比之下,fillMaxSize()
将参与定义Box的最终尺寸。
less
@Composable
fun AndroidAliensGameOverBox() {
Box(
// ...
) {
AndroidAliensRow(...)
Spacer(
modifier = Modifier
.matchParentSize()// Doesn't impact Box's final size
.fillMaxSize()// Does impatc Box's final size
.background(color = Color.Gray.copy(alpha = .7f)
)
Text(
text = "GAME OVER"
// ...
)
}
}

你可能会注意到,matchParentSize()
仅在可组合的Box内起作用。为什么以及如何呢?
一个非常重要的修饰符 组合范围(Modifier scope safety)中的概念。
组合中的安全性有一些修饰仅在应用与某些可组合项的子项才有意义。组合通过custom Scope强制执行,这就是如何匹配父级代销仅在BoxScope和weight以及RowScope和ColumnScope中可用。
kotlin
Box {
BoxChild(
modifier = Modifier
.matchParentSize()
)
}
interface BoxScope {
// ...
fun Modifier.matchParentSize(): Modifier
}
kotlin
Box {
RowChild(
modifier = Modifier
.weight(...)
)
}
interface RowScope {
// ...
fun Modifier.weight(...): Modifier
}
这可以防止您添加在其他地方不起作用的修改器,并节省您反复试验。
当我们像带有当前分数和生命值的标题一样玩时,我们的迷你游戏肯定可以从有关游戏进度的一些信息中收益,左侧和底部有一个用于开始或炸宁游戏的按钮,该按钮将游戏的其余内容包裹。

这可能会提醒您类似顶部和底部栏的东西,其中为这些类型的标题和按钮放置了标题和按钮。Android中常见UI模式,compose提供了可组合使用的多功能组合,因此我们来讨论Material Components
.

depakompose
提供了material设计的实现,是一个用于创建数字界面的综合设计系统material组件(例如:Button
、Cards
、Switches
、Layouts
),它们可以作为可组合函数一起使用。它们代表用于创建用户界面的交互式构建块,有很多可供选择,因此需要看看Material API
来进行参考。
在我们添加标题作为顶部和按钮作为底部的情况下栏组成该Material提供了Scaffold
布局,其中包含用于各组件布局为常见屏幕图案的插槽,

kotlin
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
InfoHeader(...)
},
bottomBar = {
Button(
onClick = {...}
// ...
) {
Text(
text = "PRESS START",
// ...
)
}
}
) {
AndroidAliens(...)
}
}
Scaffold可组合参数接受通用可组合lambda,您可以传入你想要的任何类型可组合项。
less
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {}
// ...
)
scss
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
ComposeShipRow(...)
},
bottomBar = {
AndroidAliensRow(...)
}
) {
AndroidAliens(...)
}
}

这个开放槽概念称为槽API,在Compose Scaffold中大量使用
scss
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
Shot1(...)
},
bottomBar = {
Slot2(...)
}
) {
Slot3(...)
}
}
scss
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
Shot1(...)
},
bottomBar = {
Slot2(...)
},
content = {
Slot3(...)
}
)
}

还接受FloatingActionButton
、SnackBarHost
、drawerContent
等其他自定义选项。
kotlin
@Composable
fun AndroidAliensWithInfo() {
Scaffold(
floatingActionButton = {
},
snackbarHost = {
},
drawerContent = {
}
// ...
)
}
目前我们在屏幕上以简单的SVG形式显示的外星飞船数量非常有限

但如果我们有数百或数千,又会怎样?动画Slender Invasion可能会导致一些严重的卡顿。

在这种情况下,从后端立即加载所有内容,特别是如果它包含大型数据集,大量图像或视频会影响应用程序的性能,如果您可以加载所有内容,该怎么办?

滚动时一点点按需内容切入我个人最爱的Lazy组件并组合以构建绿色Android外星人的快速lazy grid。我们将使用LazyVerticalGrid
可组合Lazy组件在屏幕上可见时渲染可滚动的项目列表屏幕以帮助您的应用程序的性能。
kotlin
@Composable
fun AndroidAliensGrid(modifier: Modifier = Modifier) {
LazyVerticalGrid(
modifier = modifier
) {
AndroidAlien(...)
}
}

在网格上设置固定数量的列,并向其中添加50个Android外星人项目,
scss
@Composable
fun AndroidAliensGrid(modifier: Modifier = Modifier) {
LazyVerticalGrid(
modifier = modifier,
column = GridCells.Fixed(5)
) {
items(50) {
AndroidAlien(...)
}
}
}

还有许多其他Lazy组件可供选择
- LazyHorizontalGrid
- LazyColumn
- LazyRow
- Staggered grid(交错网格)
- LazyVerticalStaggeredGrid
- LazyHorizontalStaggeredGrid
4、总结
我们已经涵盖了很多内容,从组合Layout和Modifier的基础知识,以及它们的相互协作,提供了那些开箱即用API,Material Compose和插槽API,以及如何在构建我们自己的迷你游戏时按需添加内容。