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

布局,说白了就是决定「谁站在哪」的问题。

在传统 Android 里,我们用 XML 写布局,LinearLayout、RelativeLayout、FrameLayout 这些。到了 Compose,思路变了——一切皆函数。你调用 Column,它就纵向排列;你调用 Row,它就横向排列。听起来简单,但实际用起来,坑也不少。

我个人习惯是,先把布局拆成「线性」和「自由」两大类。线性布局用 Column/Row,自由布局用 Box 或 ConstraintLayout。列表类用 LazyColumn/LazyRow。嗯,咱们一个一个说。

13.1 Column、Row、Box:三剑客

这三个是最基础的布局组件。我刚开始学 Compose 时,觉得它们跟 LinearLayout 和 FrameLayout 差不多。后来发现,Compose 的布局机制更灵活,也更「函数式」。

13.1.1 Column:纵向排列

Column 会把子组件从上往下排。你想想看,一个聊天列表、一个设置页面,基本都是纵向的。

@Composable
fun MyColumnExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("第一行")
        Text("第二行")
        Button(onClick = { /* 点击事件 */ }) {
            Text("点击我")
        }
    }
}

这里有两个关键参数:verticalArrangement 控制纵向间距,horizontalAlignment 控制横向对齐。我在项目中遇到过一个问题——子组件高度不固定时,SpaceEvenly 会均匀分配剩余空间,但如果你用了 weight 修饰符,那分配逻辑就变了。嗯,这里要注意。

小技巧:如果你想让某个子组件占满剩余空间,用 Modifier.weight(1f)。这跟 LinearLayout 的 layout_weight 是一个道理。

13.1.2 Row:横向排列

Row 跟 Column 对称,只是方向不同。横向排列,适合做按钮栏、标签栏。

@Composable
fun MyRowExample() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("左")
        Text("中")
        Text("右")
    }
}

说白了,Row 就是 Column 旋转 90 度。参数名从 vertical 变成 horizontal,但逻辑一模一样。

13.1.3 Box:层叠布局

Box 允许子组件叠在一起。比如一个头像上盖一个「VIP」角标,或者一个图片上覆盖一层半透明文字。

@Composable
fun MyBoxExample() {
    Box(
        modifier = Modifier.size(200.dp),
        contentAlignment = Alignment.Center
    ) {
        // 底层:图片
        Image(
            painter = painterResource(id = R.drawable.avatar),
            contentDescription = "头像",
            modifier = Modifier.fillMaxSize()
        )
        // 顶层:文字
        Text(
            text = "VIP",
            color = Color.White,
            style = MaterialTheme.typography.h6,
            modifier = Modifier
                .align(Alignment.TopEnd)
                .background(Color.Red, shape = RoundedCornerShape(4.dp))
                .padding(4.dp)
        )
    }
}

我曾经在做一个直播应用时,用 Box 实现了「主播头像 + 在线状态圆点 + 等级徽章」三层叠加。嗯,Box 的 align 修饰符可以精确控制每个子组件的位置,非常方便。

注意:Box 的子组件如果都用了 Modifier.fillMaxSize(),那它们会完全重叠。如果你想要「并排」效果,请用 Column 或 Row。

13.2 ConstraintLayout:复杂布局的救星

当布局嵌套超过三层,或者需要「A 的右边对齐 B 的左边」这种相对关系时,Column/Row 就有点力不从心了。这时候,ConstraintLayout 就派上了用场。

我刚开始觉得 Compose 的 ConstraintLayout 跟 XML 版差不多,但用起来发现——它更灵活,但也更啰嗦。你需要先创建引用,再设置约束。

@Composable
fun MyConstraintLayoutExample() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        // 1. 创建引用
        val (textA, textB, button) = createRefs()

        // 2. 定义组件
        Text(
            text = "A",
            modifier = Modifier.constrainAs(textA) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(parent.start)
            }
        )
        Text(
            text = "B",
            modifier = Modifier.constrainAs(textB) {
                top.linkTo(textA.bottom, margin = 8.dp)
                start.linkTo(textA.end, margin = 8.dp)
            }
        )
        Button(
            onClick = { },
            modifier = Modifier.constrainAs(button) {
                top.linkTo(textB.bottom, margin = 16.dp)
                end.linkTo(parent.end)
            }
        ) {
            Text("确定")
        }
    }
}

你看,每个组件都需要 constrainAs 块,里面写约束条件。说实话,写起来比 XML 版要繁琐一些,但好处是——所有逻辑都在代码里,没有「隐式依赖」。

核心原则:ConstraintLayout 适合「相对位置复杂」的场景。如果你的布局只是简单的上下排列,用 Column 就够了。别为了炫技而用 ConstraintLayout。

13.3 LazyColumn 与 LazyRow:高效列表

列表是移动应用的核心。聊天记录、商品列表、新闻流……全是列表。在 Compose 里,我们用 LazyColumn(纵向列表)和 LazyRow(横向列表)来实现。

为什么叫「Lazy」?因为它只渲染当前可见的项。你滑到哪,它渲染到哪。这跟 RecyclerView 的 ViewHolder 复用机制是一个道理。

13.3.1 基本用法

@Composable
fun MyLazyColumnExample() {
    val items = listOf("苹果", "香蕉", "橘子", "西瓜", "葡萄")

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(items) { fruit ->
            Text(
                text = fruit,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color.LightGray, shape = RoundedCornerShape(8.dp))
                    .padding(16.dp)
            )
        }
    }
}

这里有个细节:contentPadding 是列表整体的内边距,Arrangement.spacedBy 是项与项之间的间距。我建议你养成习惯,用 spacedBy 而不是在每个 item 里加 padding,这样更干净。

13.3.2 不同类型的 Item

真实项目中,列表里往往有多种类型。比如一个聊天列表,有「文字消息」「图片消息」「系统提示」三种类型。LazyColumn 支持用 itemscontentType 参数来区分。

@Composable
fun MyMixedListExample() {
    val messages = listOf(
        Message("text", "你好"),
        Message("image", "https://example.com/photo.jpg"),
        Message("system", "你已加入群聊")
    )

    LazyColumn {
        items(
            items = messages,
            contentType = { it.type } // 告诉 Compose 类型不同,不要复用
        ) { message ->
            when (message.type) {
                "text" -> TextMessageView(message)
                "image" -> ImageMessageView(message)
                "system" -> SystemMessageView(message)
            }
        }
    }
}

为什么要传 contentType?因为 Compose 默认会复用 item 的缓存。如果类型不同还复用,界面会闪。嗯,我曾经踩过这个坑——列表里文字和图片交替出现时,图片突然变成文字了。加上 contentType 就解决了。

13.3.3 LazyRow:横向滑动

LazyRow 跟 LazyColumn 对称,只是方向不同。适合做横向滚动的标签页、推荐商品列表。

@Composable
fun MyLazyRowExample() {
    val tags = listOf("推荐", "热门", "最新", "附近", "关注")

    LazyRow(
        modifier = Modifier.fillMaxWidth(),
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(tags) { tag ->
            Chip(text = tag, onClick = { /* 点击切换 */ })
        }
    }
}

你看,参数名从 vertical 变成 horizontal,其他都一样。

性能提示:LazyColumn 和 LazyRow 内部已经做了高效的缓存和回收。你不需要像 RecyclerView 那样写 Adapter 和 ViewHolder。但要注意——不要在 item 的 Composable 函数里做耗时操作,比如网络请求。那会卡 UI。

13.4 知识体系总览

下面这张图,是我自己总结的 Compose 布局选择逻辑。你照着这个思路走,基本不会选错。

Compose 布局选择决策树 开始布局 是列表吗? 不是列表 纵向?→ LazyColumn 横向?→ LazyRow 线性排列? 层叠/自由? 纵向?→ Column 横向?→ Row 简单层叠?→ Box 复杂约束?→ ConstraintLayout 总结:先判断是不是列表 → 再判断方向 → 最后选具体组件 Column/Row 解决 80% 场景,Box 解决 10%,ConstraintLayout 解决 10%

13.5 避坑指南

最后,分享几个我实际项目中踩过的坑。

  • Column 嵌套 LazyColumn:千万不要把 LazyColumn 放在 Column 里,除非你给 LazyColumn 固定高度。否则 Compose 不知道 LazyColumn 该占多少空间,会导致测量异常。我曾经因为这个,列表只显示了一行。
  • Modifier 顺序:Modifier.fillMaxWidth().padding(16.dp)Modifier.padding(16.dp).fillMaxWidth() 效果不同。前者是先填满再缩进,后者是先缩进再填满。嗯,顺序很重要。
  • LazyColumn 的 key:如果你列表里的数据有唯一 ID,一定要传 key 参数。否则列表滑动时,Compose 会按位置复用,导致动画错乱或状态丢失。
一句话总结:Column/Row 解决 80% 的布局需求,Box 解决层叠,ConstraintLayout 解决复杂相对位置,LazyColumn/LazyRow 解决列表。选对工具,事半功倍。

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