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 修饰符可以精确控制每个子组件的位置,非常方便。
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 版要繁琐一些,但好处是——所有逻辑都在代码里,没有「隐式依赖」。
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 支持用 items 的 contentType 参数来区分。
@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,其他都一样。
13.4 知识体系总览
下面这张图,是我自己总结的 Compose 布局选择逻辑。你照着这个思路走,基本不会选错。
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 会按位置复用,导致动画错乱或状态丢失。