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 控制纵向间距,horizontalAlignment 控制横向对齐。这两个参数你得多用用,尤其是 Arrangement.SpaceBetween 和 Arrangement.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)
)
}
}
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 一步到位。我个人习惯在需要「相对定位」时优先考虑它,比如「这个按钮要贴在右下角」或者「这两个文本要对齐」。
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 同理,只是方向变成了横向。适合做横向滚动的标签栏、图片轮播等。
contentPadding 和 Arrangement.spacedBy 搭配使用,可以轻松实现列表的边距和间距。别在 item 里加 padding,那样会破坏统一性。
知识体系总览
下面这张图帮你理清这六个布局组件的定位和适用场景:
如何选择?我的决策流程
每次拿到一个布局需求,我会按这个顺序思考:
- 数据量多大?超过 20 项?直接上 LazyColumn 或 LazyRow。
- 组件之间有没有相对位置关系?比如「A 在 B 的右边,C 在 A 的下方」?有的话用 ConstraintLayout。
- 是不是简单的从上往下或从左往右?用 Column 或 Row,别想复杂了。
- 有没有组件需要叠在一起?比如头像上加角标?用 Box。
嗯,这六个组件你掌握了,Compose 布局这块基本就通了。剩下的就是多写、多调、多踩坑。我在项目里用 LazyColumn 时,曾经忘了加 key 参数,结果列表更新时动画全乱了——后来养成了习惯,只要列表项可能变化,一定给 items 传 key。
好了,布局这块就聊到这儿。你动手试试,把今天讲的每个组件都跑一遍,感受一下它们的差异。代码写多了,自然就形成肌肉记忆了。
公众号:蓝海资料掘金营,微信deep3321