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)
    }
)
我的经验: 当布局嵌套超过 3 层时,我就开始考虑用 ConstraintLayout 了。它虽然代码量看起来多,但渲染性能比多层嵌套好得多。

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("加载更多")
        }
    }
}
避坑指南: 我曾经在 LazyColumn 里嵌套了另一个 LazyColumn,结果滑动卡成 PPT。记住:LazyColumn 不能嵌套 LazyColumn。如果非要嵌套,用 Column 代替内部的 LazyColumn。

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,屏幕宽就自动算出列数。我在做图片网格展示时特别喜欢用这个,适配各种屏幕尺寸。

注意: LazyVerticalGrid 和 LazyColumn 一样,也是懒加载的。所以性能优化原则通用。

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)
            }
        }
    }
}

这段代码看起来有点复杂,但核心逻辑就两步:

  1. 测量:调用 measure() 获取每个子组件的尺寸。
  2. 摆放:调用 placeRelative() 确定每个子组件的位置。
我的建议: 自定义布局虽然强大,但能不写就不写。因为一旦写错,调试起来很痛苦。我一般先用标准布局组合,实在不行再上自定义。

4.5 本章知识体系

下面这张图帮你理清这几种布局的关系和适用场景:

布局基础(下)知识体系 布局基础(下) ConstraintLayout LazyColumn/LazyRow Grid布局 自定义布局 链式约束 辅助线 懒加载 key 优化 Fixed/Adaptive 懒加载 测量 + 摆放 Layout 函数 选择原则:简单用 Column/Row,复杂用 Constraint,列表用 Lazy,特殊需求自定义

4.6 小结

这一节的内容不少,但都是实战中高频使用的。我个人的经验是:

  • 遇到复杂嵌套,先想想能不能用 ConstraintLayout 简化。
  • 列表数据超过 20 条,果断上 LazyColumn 或 LazyVerticalGrid。
  • 自定义布局是最后的手段,但掌握它能让你的 UI 能力上一个台阶。

嗯,布局基础到这里就告一段落了。下一节我们会进入更精彩的世界——状态管理与数据流。到时候你会看到 Compose 真正的威力。


公众号:蓝海资料掘金营,微信deep3321