Compose 布局系统
Column / Row / Box 三大容器、Modifier 链式调用、Spacer、ConstraintLayout Compose、Scaffold 结构、verticalScroll 与 BoxWithConstraints 响应式
学习目标
- 熟练使用 Column / Row / Box 的排列与对齐参数
- 理解 Modifier 链式调用顺序的意义
- 掌握 Scaffold 顶 / 底 / FAB 结构与 contentWindowInsets
- 用 verticalScroll 与 BoxWithConstraints 处理滚动与响应式
- 了解 ConstraintLayout Compose 与传统布局的取舍
学习目标
布局是任何 UI 框架的基石。Compose 的布局系统比传统 View 更简洁:没有 XML、没有 LayoutParams、没有各种 LayoutXXX,只有 Composable 函数 + Modifier。
读完本章你将能够:
- 用
Column/Row/Box拼出任意静态布局; - 理解
Modifier链式调用的“顺序敏感”,能正确组合padding/background/size/clickable; - 用
Scaffold一次搭好TopAppBar/BottomBar/FloatingActionButton; - 用
verticalScroll处理可滚动内容; - 用
BoxWithConstraints实现响应式(窄 / 宽屏切换布局); - 了解
ConstraintLayoutCompose 的定位与何时用它。
三大容器:Column / Row / Box
1. Column:垂直排列
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize(),
) {
Text("第一行")
Text("第二行")
Text("第三行")
}
| 参数 | 含义 |
|---|---|
verticalArrangement |
子元素在垂直方向的对齐与间距 |
horizontalAlignment |
子元素在水平方向的对齐 |
常用 Arrangement:
Arrangement.Top/Center/Bottom:按起点 / 中 / 末对齐;Arrangement.spacedBy(8.dp):每两个子之间留 8dp;Arrangement.spacedBy(8.dp, alignment = Alignment.Center):留间距 + 居中;Arrangement.SpaceBetween/SpaceEvenly/SpaceAround:等距分布。
2. Row:水平排列
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.Person, contentDescription = "用户")
Spacer(modifier = Modifier.width(4.dp))
Text("Alice")
}
注意 Row 的对齐轴颠倒:horizontalArrangement 控制水平方向,verticalAlignment 控制垂直。
3. Box:层叠 + 对齐
Box(
modifier = Modifier.size(120.dp),
contentAlignment = Alignment.Center,
) {
Image(painter = painterResource(R.drawable.bg), contentDescription = null)
Text(
text = "标签",
modifier = Modifier.align(Alignment.BottomStart),
)
}
Box 内部子组件默认从左上开始堆叠,可用 Modifier.align(...) 单独指定每个子对齐位置。
Box 还可设 propagateMinConstraints,决定父级 Box 的 min 约束是否传给子。
Modifier 链式调用
Modifier 是 Compose 中修饰一个 Composable 的所有“附加属性”的载体:padding、background、size、点击、滚动、对齐……它的写法是链式:
Text(
text = "Hello",
modifier = Modifier
.padding(16.dp)
.background(Color.LightGray)
.size(200.dp, 80.dp)
.clickable { /* onClick */ },
)
1. 顺序敏感
Modifier 链的顺序就是应用顺序,前后颠倒效果完全不同。这是 Compose 新手最容易踩的坑:
// 先 padding 再 background:背景只覆盖内容区,padding 外是透明的
Modifier.padding(16.dp).background(Color.Red)
// 先 background 再 padding:背景覆盖到 padding 外,padding 内是内容
Modifier.background(Color.Red).padding(16.dp)
记忆技巧:把 Modifier 链想象成一层层从外向内的“包装”。先写的在外层,后写的在内层。
2. 常用 Modifier 速览
| Modifier | 作用 |
|---|---|
padding(all / horizontal / vertical / start / top / end / bottom) |
内边距 |
background(color, shape) |
背景 |
fillMaxSize() / fillMaxWidth() / fillMaxHeight() |
填满 |
size(width, height) / width / height |
固定尺寸 |
wrapContentSize(align) |
内容自适应 |
clickable(onClick) / pointerInput |
点击 / 手势 |
clip(shape) / border(width, color, shape) |
圆角与边框 |
alpha(value) |
透明度 |
offset(x, y) |
位移 |
align(alignment) |
在 Box / Column / Row 中对齐 |
weight(weight) |
在 Row / Column 中按比例分配空间 |
3. Modifier 不可变 + 拼接
val baseModifier = Modifier
.fillMaxWidth()
.padding(16.dp)
// 复用
Text("A", modifier = baseModifier.background(Color.Gray))
Text("B", modifier = baseModifier.background(Color.Yellow))
Modifier 是不可变的,每次 .xxx() 都返回新实例,安全复用。
4. weight:比例分配
只在 Row / Column 的直接子级有效:
Row {
Text("左", modifier = Modifier.weight(1f))
Text("右", modifier = Modifier.weight(2f))
}
// 左:右 = 1:2
Modifier.weight(1f, fill = true) 中 fill 决定是否撑满剩余空间(默认 true)。
5. 自定义 Modifier
fun Modifier.debugBorder(color: Color = Color.Red): Modifier =
this.then(Modifier.border(1.dp, color))
// 使用
Text("调试", modifier = Modifier.debugBorder())
复杂自定义可用 composed { } 让 Modifier 内部访问 remember 与状态。
Spacer
Column {
Text("A")
Spacer(modifier = Modifier.height(8.dp))
Text("B")
}
Spacer 在视觉上留空,比 padding 在多子组件场景更直观。
ConstraintLayout Compose
ConstraintLayout 在传统 View 中是性能王者,但在 Compose 中由于 Column / Row / Box 已能拼出绝大多数布局,且 Compose 自身测量机制更高效,ConstraintLayout Compose 不再是首选。
它的用途:
- 复杂约束关系(多个组件相对位置相互依赖);
- 从传统 ConstraintLayout XML 迁移时保持等价。
依赖:
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
@Composable
fun ConstraintLayoutExample() {
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
val (logo, title, subtitle) = createRefs()
Image(
painter = painterResource(R.drawable.logo),
contentDescription = null,
modifier = Modifier
.size(64.dp)
.constrainAs(logo) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start, margin = 16.dp)
},
)
Text(
text = "标题",
modifier = Modifier.constrainAs(title) {
top.linkTo(logo.top)
start.linkTo(logo.end, margin = 8.dp)
},
)
Text(
text = "副标题",
modifier = Modifier.constrainAs(subtitle) {
top.linkTo(title.bottom)
start.linkTo(title.start)
},
)
}
}
建议:能用
Column / Row / Box表达的就别用 ConstraintLayout,可读性更高、维护更简单。只在约束真复杂时用。
Scaffold:App 骨架
Scaffold 是 Material3 提供的“页面骨架”,预置了 TopAppBar / BottomBar / FAB / Snackbar 的位置与 WindowInsets 处理:
@Composable
fun AppScaffold() {
var clickCount by remember { mutableStateOf(0) }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(
title = { Text("首页") },
navigationIcon = {
IconButton(onClick = { /* 抽屉 */ }) {
Icon(Icons.Default.Menu, contentDescription = "菜单")
}
},
)
},
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = true,
onClick = { },
icon = { Icon(Icons.Default.Home, contentDescription = null) },
label = { Text("首页") },
)
}
},
floatingActionButton = {
FloatingActionButton(onClick = {
clickCount++
scope.launch {
snackbarHostState.showSnackbar("点击 $clickCount 次")
}
}) {
Icon(Icons.Default.Add, contentDescription = "添加")
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
contentWindowInsets = WindowInsets.statusBars,
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
Text("内容区")
}
}
}
要点:
Scaffold接收 6 个@Composable槽:topBar/bottomBar/floatingActionButton/snackbarHost/content(lambda) /drawerContainer;innerPadding自动处理TopAppBar与状态栏 / 导航栏的WindowInsets,不要手动加 padding,否则会双重留白;contentWindowInsets控制 content 区域消费哪些 insets(默认消费全部)。
滚动:verticalScroll / horizontalScroll / lazy
1. 单组件可滚动
短列表 / 表单常用 verticalScroll:
val state = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state),
) {
repeat(50) { i ->
Text("第 $i 行", modifier = Modifier.padding(8.dp))
}
}
verticalScroll 接收 ScrollState,可保存 / 恢复滚动位置。它的特点是:所有子节点一次性创建(不像 LazyColumn 复用),适合 少量内容。
2. 嵌套滚动
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// 标题
Text("Banner", modifier = Modifier.height(200.dp))
// 横向滚动
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
) {
repeat(10) { i ->
Text("卡片 $i", modifier = Modifier.padding(8.dp))
}
}
}
不同方向滚动天然支持嵌套,不需要特殊配置。
3. 大列表用 LazyColumn
数据量大(超过一屏)时必须用 LazyColumn / LazyRow,它会按需创建并复用节点(下章详讲)。
BoxWithConstraints:响应式
BoxWithConstraints 暴露父容器施加的尺寸约束,让你在窄屏 / 宽屏切换布局:
@Composable
fun ResponsiveLayout(items: List<String>) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val isWide = maxWidth > 600.dp
if (isWide) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items.forEach { Text(it) }
}
} else {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items.forEach { Text(it) }
}
}
}
}
maxWidth / minWidth / maxHeight / minHeight 都是 Dp,可读也可比较。
实际项目中常用的“平板分屏 / 手机单列”切换,就是 BoxWithConstraints + 阈值 的组合。
综合示例:登录表单
把以上知识串成一个完整表单:
@Composable
fun LoginForm(onSubmit: (String, String) -> Unit) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
val state = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text("登录", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(8.dp))
TextField(
value = username,
onValueChange = { username = it },
label = { Text("用户名") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("密码") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth(),
)
Button(
onClick = { onSubmit(username, password) },
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
) { Text("提交") }
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
fun LoginFormPreview() {
MaterialTheme {
LoginForm(onSubmit = { u, p -> println("$u / $p") })
}
}
观察:
verticalScroll(state)让键盘弹出时表单仍可滚动;Arrangement.spacedBy让子元素自动留间距,省Spacer;Modifier.fillMaxWidth()让输入框占满;@Preview配widthDp / heightDp模拟真机尺寸。
常见坑与最佳实践
- Modifier 顺序写反:
padding与background/clickable顺序错位导致样式不对。链 = 包装顺序,先写在外层。 Modifier.weight在 Column 外用:weight 只在Column/Row直接子级有效,写在Box子级或嵌套层级里会被忽略。需要拉到外层。fillMaxWidth之后又加wrapContentWidth:两者冲突,后者覆盖前者。布局抖动通常是 modifier 写法不一致。verticalScroll包 LazyColumn:LazyColumn 自带滚动,再被verticalScroll包会被编译器警告甚至崩溃。两者择一。innerPadding被忽略:用Scaffold必须把 content lambda 收到的 padding 应用到根,否则状态栏 / 顶栏会盖住内容。- 嵌套 Column 又用 weight:外层 Column 与内层 Column 都用 weight 时高度会爆。复杂层级要拆
BoxWithConstraints或换 ConstraintLayout。 - 用
Box装weight:weight 在 Box 子级不存在。Box 内对齐靠Modifier.align(...)。 - 手动处理 statusBars:Android 13+ 状态栏 / 导航栏用
WindowInsets.statusBars/navigationBars处理,不要写死Modifier.padding(top = 24.dp)。 verticalScroll放大列表:少量内容用verticalScroll,数据量大用LazyColumn才不会爆内存。- Modifier 重复创建:高频调用的 Modifier 建议
val缓存,或写到顶层Modifier。重组时反复 new 会有微小开销。
章节小结
Column/Row/Box是三大容器:注意Arrangement与Alignment轴对应;Modifier链是顺序敏感的“包装”,先写的在外层;常用padding / background / size / fillMax / clickable / align / weight;Spacer留白比反复padding更直观;weight在 Row / Column 子级按比例分;ConstraintLayoutCompose 不是首选,仅在约束真复杂或迁移 XML 时使用;Scaffold提供顶 / 底 / FAB / Snackbar 骨架,content lambda 的innerPadding必须应用到根;verticalScroll适合少量内容,LazyColumn适合长列表;BoxWithConstraints用maxWidth > N.dp切换响应式布局。
下一章预告
下一章我们走进 Compose 常用组件:Text / Button / OutlinedButton / TextButton、TextField / OutlinedTextField、Image / Icon / Card / Slider / Checkbox / RadioButton / Switch、AlertDialog 与 Snackbar(SnackbarHostState)。掌握这些 M3 组件后,你就能拼出绝大多数业务表单与卡片。