41、Compose布局:Column、Row、Box、ConstraintLayout、LazyColumn与LazyRow

Compose 的布局系统,说白了就是一套「搭积木」的规则。你不需要写 XML,直接用 Kotlin 代码把组件堆起来就行。我刚开始从传统 View 系统切过来时,总觉得少了点什么——后来发现,其实是少了「束缚」。Compose 的布局更灵活,也更考验你对基础组件的理解。

今天咱们就把六个核心布局组件掰开揉碎讲清楚。Column、Row、Box 是基础三件套,ConstraintLayout 是复杂布局的救星,LazyColumn 和 LazyRow 则是列表场景的标配。嗯,一个一个来。

Column:纵向排列,最常用的容器

Column 就是把子组件从上往下排。你想想看,一个典型的列表页、表单页,哪个不是从上往下走的?

@Composable
fun ColumnExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("第一行")
        Text("第二行")
        Button(onClick = { /* 点击事件 */ }) {
            Text("按钮")
        }
    }
}
我的习惯:verticalArrangement 用 Arrangement.spacedBy() 比在每个子组件上加 padding 更干净。间距统一,改起来也方便。

verticalArrangement 控制纵向间距,horizontalAlignment 控制横向对齐。这两个参数你得多用用,尤其是 Arrangement.SpaceBetweenArrangement.SpaceEvenly,在需要均匀分布子组件时特别好使。

Row:横向排列,跟 Column 对称

Row 就是把子组件从左往右排。跟 Column 几乎一模一样,只是方向不同。

@Composable
fun RowExample() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(Icons.Default.Home, contentDescription = "首页")
        Icon(Icons.Default.Favorite, contentDescription = "收藏")
        Icon(Icons.Default.Settings, contentDescription = "设置")
    }
}

我在项目中遇到过一个问题:Row 里的子组件高度不一致时,默认是顶部对齐,看起来特别别扭。记得加上 verticalAlignment = Alignment.CenterVertically,瞬间就舒服了。

Box:层叠布局,像 FrameLayout

Box 允许子组件叠在一起。说白了就是「堆叠」,后面的组件会盖在前面的上面。适合做头像上的角标、图片上的文字覆盖等场景。

@Composable
fun BoxExample() {
    Box(
        modifier = Modifier.size(120.dp),
        contentAlignment = Alignment.Center
    ) {
        // 底层:圆形头像
        Image(
            painter = painterResource(id = R.drawable.avatar),
            contentDescription = "头像",
            modifier = Modifier
                .size(100.dp)
                .clip(CircleShape)
        )
        // 上层:VIP 角标
        Text(
            text = "VIP",
            color = Color.White,
            fontSize = 12.sp,
            modifier = Modifier
                .align(Alignment.TopEnd)
                .background(Color.Red, RoundedCornerShape(4.dp))
                .padding(horizontal = 4.dp, vertical = 2.dp)
        )
    }
}
我曾经踩过的坑:Box 默认的 contentAlignment 是 TopStart,如果你不指定,子组件会全部挤在左上角。想要居中?记得显式传 contentAlignment = Alignment.Center

ConstraintLayout:复杂布局的救星

当布局嵌套超过三层,或者组件之间关系复杂时,Column/Row 就有点力不从心了。这时候我会掏出 ConstraintLayout。它允许你给每个组件设置约束,比如「A 在 B 的右边,C 在 A 的下方」。

@Composable
fun ConstraintLayoutExample() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        // 1. 创建引用
        val (avatar, name, badge) = createRefs()

        // 2. 设置约束
        Image(
            // ... 头像组件
            modifier = Modifier.constrainAs(avatar) {
                top.linkTo(parent.top)
                start.linkTo(parent.start)
                width = Dimension.value(48.dp)
                height = Dimension.value(48.dp)
            }
        )
        Text(
            text = "张三",
            modifier = Modifier.constrainAs(name) {
                top.linkTo(avatar.top)
                start.linkTo(avatar.end, margin = 12.dp)
            }
        )
        Text(
            text = "认证作者",
            modifier = Modifier.constrainAs(badge) {
                top.linkTo(name.bottom, margin = 4.dp)
                start.linkTo(name.start)
            }
        )
    }
}

你想想看,如果用 Column+Row 实现上面这个布局,得嵌套多少层?ConstraintLayout 一步到位。我个人习惯在需要「相对定位」时优先考虑它,比如「这个按钮要贴在右下角」或者「这两个文本要对齐」。

核心要点:ConstraintLayout 的 createRefs() 创建引用,constrainAs 设置约束。记住三步:创建引用 → 设置约束 → 绑定组件。

LazyColumn 与 LazyRow:列表场景的标配

如果你要展示一个列表,千万别用 Column 包一个 for 循环。为什么?因为 Column 会一次性渲染所有子组件,列表长了直接卡死。LazyColumn 只渲染可见区域内的组件,滚动时回收不可见的,性能天差地别。

@Composable
fun LazyColumnExample() {
    val items = (1..100).toList()

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // 使用 items 函数
        items(items) { index ->
            ListItem(index = index)
        }

        // 也可以加头部和尾部
        item {
            Text("列表头部", fontWeight = FontWeight.Bold)
        }
        item {
            Text("列表尾部", textAlign = TextAlign.Center)
        }
    }
}

LazyRow 同理,只是方向变成了横向。适合做横向滚动的标签栏、图片轮播等。

我的经验:LazyColumn 的 contentPaddingArrangement.spacedBy 搭配使用,可以轻松实现列表的边距和间距。别在 item 里加 padding,那样会破坏统一性。

知识体系总览

下面这张图帮你理清这六个布局组件的定位和适用场景:

Compose 六大布局组件 基础布局 Column 纵向排列 Row 横向排列 Box 层叠布局 复杂布局 ConstraintLayout 相对定位 / 复杂关系 列表布局 LazyColumn 纵向列表 / 懒加载 LazyRow 横向列表 / 懒加载 选择原则:简单列表用 Column/Row,复杂关系用 ConstraintLayout,大量数据用 LazyColumn/LazyRow

如何选择?我的决策流程

每次拿到一个布局需求,我会按这个顺序思考:

  1. 数据量多大?超过 20 项?直接上 LazyColumn 或 LazyRow。
  2. 组件之间有没有相对位置关系?比如「A 在 B 的右边,C 在 A 的下方」?有的话用 ConstraintLayout。
  3. 是不是简单的从上往下或从左往右?用 Column 或 Row,别想复杂了。
  4. 有没有组件需要叠在一起?比如头像上加角标?用 Box。
一句话总结:Column/Row 解决 80% 的日常布局,Box 解决层叠需求,ConstraintLayout 解决复杂关系,LazyColumn/LazyRow 解决列表性能问题。别用错,也别过度设计。

嗯,这六个组件你掌握了,Compose 布局这块基本就通了。剩下的就是多写、多调、多踩坑。我在项目里用 LazyColumn 时,曾经忘了加 key 参数,结果列表更新时动画全乱了——后来养成了习惯,只要列表项可能变化,一定给 itemskey

好了,布局这块就聊到这儿。你动手试试,把今天讲的每个组件都跑一遍,感受一下它们的差异。代码写多了,自然就形成肌肉记忆了。


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