4、布局基础(下):ConstraintLayout、LazyColumn与LazyRow、Grid布局、自定义布局
上一节我们把 Compose 里最常用的几个布局讲完了。这一节,咱们来点更进阶的。说实话,这些布局在实际项目中几乎天天见。我个人习惯把 ConstraintLayout 当作「救火队员」——当其他布局搞不定复杂嵌套时,它往往能一招制敌。而 LazyColumn 和 LazyRow,说白了就是 RecyclerView 的 Compose 版本,性能优化全靠它们。
好,咱们一个一个来。
4.1 ConstraintLayout:复杂布局的终极方案
ConstraintLayout 在 Compose 里和 XML 版本思路一致,但写法完全不同。它用 DSL 的方式来描述约束关系。你想想看,以前在 XML 里写一堆 app:layout_constraintStart_toEndOf,现在全变成 Kotlin 代码了。
先看一个最简单的例子:
@Composable
fun ConstraintLayoutDemo() {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
// 1. 创建引用
val (button, text) = createRefs()
// 2. 设置约束
Button(
onClick = { },
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
) {
Text("点击我")
}
Text(
text = "Hello",
modifier = Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 8.dp)
start.linkTo(button.start)
}
)
}
}
嗯,这里要注意:createRefs() 必须在 ConstraintLayout 的作用域内调用。我曾经见过有人把它写在外面,结果编译报错半天没找到原因。
4.1.1 链式约束与辅助线
实际项目中,我经常用链式约束来做等间距排列。比如三个按钮均匀分布:
val (btn1, btn2, btn3) = createRefs()
createHorizontalChain(btn1, btn2, btn3) {
// 链式约束的三种模式
// Spread: 均匀分布(默认)
// SpreadInside: 两端无间距
// Packed: 挤在一起
chainStyle = ChainStyle.Spread
}
辅助线(Guideline)也是个好东西。比如你想让某个组件位于屏幕 30% 的位置:
val guideline = createGuidelineFromStart(0.3f)
Text(
text = "位于30%处",
modifier = Modifier.constrainAs(textRef) {
start.linkTo(guideline)
}
)
4.2 LazyColumn 与 LazyRow:高性能列表
LazyColumn 和 LazyRow 是 Compose 里的 RecyclerView。它们只渲染可见区域内的 item,滑动时回收不可见的 item。说白了,就是「按需加载」。
基本用法很简单:
@Composable
fun SimpleLazyColumn() {
val items = (1..100).toList()
LazyColumn {
items(items) { item ->
Text(
text = "Item $item",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}
但实际项目中,我们往往需要更复杂的 item 类型。比如列表里混着标题和内容:
LazyColumn {
// 不同类型的 item
item {
Text("标题区域", style = MaterialTheme.typography.h5)
}
items(50) { index ->
ListItem(
headlineContent = { Text("第 $index 项") }
)
}
item {
Button(onClick = { }) {
Text("加载更多")
}
}
}
4.2.1 性能优化要点
LazyColumn 虽然高效,但用不好照样卡。我总结了几条经验:
- 使用 key 参数:给每个 item 一个稳定的 key,帮助 Compose 识别哪些 item 需要重组。
- 避免在 item 内做耗时计算:把计算结果缓存起来,或者用
remember记住。 - 控制 item 的复杂度:一个 item 里的组件不要太多,否则滑动时重组压力大。
LazyColumn {
items(
items = dataList,
key = { it.id } // 稳定 key
) { item ->
ComplexItem(item)
}
}
4.3 Grid 布局:LazyVerticalGrid 与 LazyHorizontalGrid
Grid 布局在 Compose 里用 LazyVerticalGrid 实现。它和 LazyColumn 类似,但可以指定列数。
@Composable
fun GridDemo() {
LazyVerticalGrid(
columns = GridCells.Fixed(2), // 固定 2 列
modifier = Modifier.fillMaxSize()
) {
items(20) { index ->
Card(
modifier = Modifier
.padding(4.dp)
.height(100.dp)
) {
Text("Item $index")
}
}
}
}
除了固定列数,还可以用 GridCells.Adaptive(minSize) 让列数自适应。比如:
columns = GridCells.Adaptive(minSize = 120.dp)
这样每列至少 120dp,屏幕宽就自动算出列数。我在做图片网格展示时特别喜欢用这个,适配各种屏幕尺寸。
4.4 自定义布局:当标准布局不够用时
有时候,标准布局满足不了需求。比如你想实现一个「流式布局」(FlowLayout),让子组件自动换行。这时候就需要自定义布局了。
Compose 的自定义布局核心是 Layout 可组合函数。它让你手动测量和摆放子组件。
@Composable
fun FlowLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 1. 测量所有子组件
val placeables = measurables.map { it.measure(constraints) }
// 2. 计算布局尺寸和位置
var x = 0
var y = 0
var rowHeight = 0
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
// 如果放不下,换行
if (x + placeable.width > constraints.maxWidth) {
x = 0
y += rowHeight
rowHeight = 0
}
placeable.placeRelative(x, y)
x += placeable.width
rowHeight = maxOf(rowHeight, placeable.height)
}
}
}
}
这段代码看起来有点复杂,但核心逻辑就两步:
- 测量:调用
measure()获取每个子组件的尺寸。 - 摆放:调用
placeRelative()确定每个子组件的位置。
4.5 本章知识体系
下面这张图帮你理清这几种布局的关系和适用场景:
4.6 小结
这一节的内容不少,但都是实战中高频使用的。我个人的经验是:
- 遇到复杂嵌套,先想想能不能用 ConstraintLayout 简化。
- 列表数据超过 20 条,果断上 LazyColumn 或 LazyVerticalGrid。
- 自定义布局是最后的手段,但掌握它能让你的 UI 能力上一个台阶。
嗯,布局基础到这里就告一段落了。下一节我们会进入更精彩的世界——状态管理与数据流。到时候你会看到 Compose 真正的威力。