Jetpack Compose 入门系列(五):自定义布局与 ConstraintLayout
学完上篇你已经能让界面"动起来"了,但遇到复杂布局还是只能 Column 套 Row、Row 套 Column,层数越嵌越深。本篇解决一个问题:当标准布局不够用时,如何自己掌控测量与排列。
一、Compose 布局原理:测量与放置
在 XML 时代,自定义布局要写一个继承 ViewGroup 的类,重写 onMeasure() 和 onLayout()——先测量子 View 多大,再把它们放到指定位置。Compose 的思路完全一样,只是写法不同:
flowchart TB
A[父 Composable<br/>拿到可用空间约束] --> B[测量所有子组件<br/>Measurable.measure]
B --> C[子组件返回自己的尺寸<br/>Measurable]
C --> D[父组件根据子组件尺寸<br/>决定自己的大小]
D --> E[放置子组件到指定位置<br/>placeable.place]
用一句话概括:先量尺寸,再定位置。不管用什么 API,底层都是这个流程。
1.1 三个核心概念
| 概念 | 含义 | 类比 XML |
|---|---|---|
Measurable | 待测量的子组件 | View 在 onMeasure 中的 child |
MeasureScope | 测量环境,提供 measure() 方法 | MeasureSpec |
Placeable | 测量完成的子组件,可以指定位置 | onLayout 中拿到 child 宽高后放置 |
1.2 Constraints:来自父组件的约束
在测量之前,父组件会给每个子组件一个 Constraints,告诉它"你最大可以多大、最小必须多大":
Constraints(
minWidth = 0, // 最小宽度
maxWidth = 1080, // 最大宽度
minHeight = 0, // 最小高度
maxHeight = 1920 // 最大高度
)
这和 XML 里的
MeasureSpec.AT_MOST、MeasureSpec.EXACTLY是同一回事,只是 Compose 用Constraints统一表达。
二、自定义 Layout Composable
2.1 基本写法
Layout 是 Compose 提供的自定义布局入口,相当于 XML 中的自定义 ViewGroup:
@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 1. 测量所有子组件
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 2. 计算自身大小
val width = placeables.maxOfOrNull { it.width } ?: 0
val height = placeables.sumOf { it.height }
// 3. 放置子组件
layout(width, height) {
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height
}
}
}
}
逐步拆解:
measurables:所有子组件,还没测量,类型是List<Measurable>constraints:父组件给的约束(最大/最小宽高)measurable.measure(constraints):测量子组件,返回Placeable(已测量,有宽高)layout(width, height) { }:声明自身大小,并在 block 里放置子组件placeable.placeRelative(x, y):把子组件放在 (x, y) 位置,Relative表示自动处理 RTL 布局
上面的代码效果等同于
Column——垂直排列子组件。但你能看到"测量 + 放置"每一步在做什么。
2.2 完整示例:纵向等间距布局
Column 的 Arrangement.SpaceEvenly 可以做到等间距,但如果需求是"第一个贴顶、最后一个贴底、中间等间距"呢?用 Column 的 Arrangement 做不到,自定义 Layout 只需几行:
@Composable
fun EqualSpaceColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val totalHeight = constraints.maxHeight
val childrenHeight = placeables.sumOf { it.height }
val spacing = if (measurables.size > 1) {
(totalHeight - childrenHeight).toFloat() / (measurables.size - 1)
} else {
0f
}
layout(constraints.maxWidth, totalHeight) {
var y = 0f
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = y.roundToInt())
y += placeable.height + spacing
}
}
}
}
使用方式和普通 Column 一样:
@Composable
fun EqualSpaceColumnDemo() {
EqualSpaceColumn(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.background(Color(0xFFF5F5F5))
) {
Text("第一个", modifier = Modifier.background(Color.Red.copy(alpha = 0.3f)))
Text("第二个", modifier = Modifier.background(Color.Green.copy(alpha = 0.3f)))
Text("第三个", modifier = Modifier.background(Color.Blue.copy(alpha = 0.3f)))
}
}
这段代码里发生了什么:
- 父组件给了
constraints.maxHeight = 300dp的约束 - 测量三个 Text,得到各自的实际高度
- 计算间距:
(总高度 - 子组件高度之和) / (子组件数 - 1) - 放置时,每个子组件的 y 坐标 = 上一个的 y + 上一个的高度 + 间距
placeRelative会自动处理 RTL(从右到左)布局。如果你用place,在 RTL 环境下 x 坐标不会自动镜像。除非有特殊需求,始终用placeRelative。
2.3 示例:瀑布流布局
瀑布流(Staggered Grid)是 Compose 标准布局里没有的,但自定义 Layout 实现起来并不复杂:
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 2,
horizontalGap: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val gapPx = horizontalGap.roundToPx()
val rowWidth = (constraints.maxWidth - gapPx * (rows - 1)) / rows
val rowConstraints = constraints.copy(
minWidth = rowWidth,
maxWidth = rowWidth
)
// 按行分组
val rowGroups = measurables.mapIndexed { index, measurable ->
val placeable = measurable.measure(rowConstraints)
RowItem(row = index % rows, placeable = placeable)
}
// 计算每行的 y 偏移量(上一行最后一个 item 的底部)
val rowHeights = IntArray(rows) { 0 }
rowGroups.forEach { item ->
val currentRowHeight = rowHeights[item.row]
rowHeights[item.row] = currentRowHeight + item.placeable.height
}
val totalHeight = rowHeights.maxOrNull() ?: 0
layout(constraints.maxWidth, totalHeight) {
// 重置每行当前 y 偏移
val currentY = IntArray(rows) { 0 }
rowGroups.forEach { item ->
val x = item.row * (rowWidth + gapPx)
val y = currentY[item.row]
item.placeable.placeRelative(x = x, y = y)
currentY[item.row] = y + item.placeable.height
}
}
}
}
private data class RowItem(
val row: Int,
val placeable: Placeable
)
使用:
@Composable
fun StaggeredGridDemo() {
val items = listOf("短文字", "这是一段比较长的文字内容", "中等", "又一段长文字,占据更多空间", "短", "最后一条")
StaggeredGrid(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
rows = 2,
horizontalGap = 8.dp
) {
items.forEach { text ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Text(
text = text,
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
这段代码里发生了什么:
- 把总宽度按列数平分,每列宽度 =
(总宽 - 间距) / 列数 index % rows决定每个 item 放在哪一列(交错排列)- 每列独立追踪当前 y 偏移,实现瀑布流效果
注意
RowItem是个data class,用于把测量结果和行号绑定在一起。这是自定义 Layout 中常见的模式——测量阶段收集信息,放置阶段使用信息。
三、Modifier.layout:自定义布局修饰符
3.1 它是什么
Layout composable 是创建一个新的布局容器。但有时候你只是想修改某个组件自身的测量或位置——比如给组件加内边距、居中偏移、等比缩放。这时用 Modifier.layout() 更合适。
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
对比一下两种方式的区别:
Layout Composable | Modifier.layout | |
|---|---|---|
| 作用对象 | 包裹多个子组件 | 修改单个组件 |
| 类比 XML | 自定义 ViewGroup | 自定义 LayoutParams |
| 典型场景 | 自定义排列方式 | 内边距、偏移、缩放等修饰 |
3.2 完整示例:自定义内边距修饰符
Compose 自带 Modifier.padding(),但我们用 Modifier.layout() 手写一个,理解它底层在做什么:
fun Modifier.customPadding(
start: Dp = 0.dp,
top: Dp = 0.dp,
end: Dp = 0.dp,
bottom: Dp = 0.dp
) = this.then(Modifier.layout { measurable, constraints ->
val startPx = start.roundToPx()
val topPx = top.roundToPx()
val endPx = end.roundToPx()
val bottomPx = bottom.roundToPx()
// 给子组件缩小可用空间
val childConstraints = constraints.copy(
minWidth = (constraints.minWidth - startPx - endPx).coerceAtLeast(0),
maxWidth = (constraints.maxWidth - startPx - endPx).coerceAtLeast(0),
minHeight = (constraints.minHeight - topPx - bottomPx).coerceAtLeast(0),
maxHeight = (constraints.maxHeight - topPx - bottomPx).coerceAtLeast(0)
)
val placeable = measurable.measure(childConstraints)
// 自身大小 = 子组件大小 + 内边距
val width = placeable.width + startPx + endPx
val height = placeable.height + topPx + bottomPx
layout(width, height) {
// 子组件位置偏移内边距
placeable.placeRelative(startPx, topPx)
}
})
使用:
@Composable
fun CustomPaddingDemo() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
) {
Text(
text = "Hello Compose",
modifier = Modifier
.customPadding(start = 16.dp, top = 24.dp)
.background(Color.Cyan.copy(alpha = 0.5f))
)
}
}
这段代码里发生了什么:
customPadding把父组件给的约束缩小(减去内边距),传给子组件- 子组件在缩小后的空间里测量自己
- 自身大小 = 子组件大小 + 内边距
- 放置子组件时偏移内边距的距离
coerceAtLeast(0)防止内边距比可用空间还大时出现负数——这和Modifier.padding()内部的处理逻辑一致。
3.3 示例:垂直居中修饰符
fun Modifier.verticalCenter() = this.then(Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(constraints.maxWidth, constraints.maxHeight) {
val y = (constraints.maxHeight - placeable.height) / 2
placeable.placeRelative(0, y)
}
})
使用:
@Composable
fun VerticalCenterDemo() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color(0xFFF0F0F0))
) {
Text(
text = "垂直居中",
modifier = Modifier.verticalCenter()
)
}
}
Modifier.layout的典型用法:不改变子组件的大小,只改变它的位置。这个思路和 CSS 的transform: translateY(50%)类似——不影响文档流,只做视觉偏移。
四、ConstraintLayout:约束布局
4.1 为什么需要 ConstraintLayout
Column + Row 能解决大部分布局,但嵌套层数多了会影响性能(每层都要测量一次),而且代码可读性差。ConstraintLayout 可以让你在一个扁平层级里完成复杂布局——和 XML 里的 ConstraintLayout 是同一个东西。
flowchart TB
subgraph 嵌套布局
A[Column] --> B[Row]
B --> C[Text]
B --> D[Image]
A --> E[Row]
E --> F[Button]
E --> G[Text]
end
subgraph ConstraintLayout
H[ConstraintLayout] --> I[Text]
H --> J[Image]
H --> K[Button]
H --> L[Text]
end
嵌套布局 -.->|扁平化| ConstraintLayout
4.2 添加依赖
gradle/libs.versions.toml:
[versions]
constraintLayout = "1.1.1"
[libraries]
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintLayout" }
app/build.gradle.kts:
dependencies {
implementation(libs.androidx.constraintlayout.compose)
}
4.3 基本用法:通过引用和约束来布局
ConstraintLayout 的核心思路是:给每个子组件创建一个引用,然后用约束把它们"绑"到相对位置上。
@Composable
fun ConstraintLayoutDemo() {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
// 创建引用
val (title, subtitle, avatar) = createRefs()
// 头像:左上角
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier
.size(60.dp)
.constrainAs(avatar) {
start.linkTo(parent.start, margin = 16.dp)
top.linkTo(parent.top, margin = 16.dp)
}
)
// 标题:头像右侧、垂直居中
Text(
text = "Jetpack Compose",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.constrainAs(title) {
start.linkTo(avatar.end, margin = 12.dp)
top.linkTo(avatar.top)
bottom.linkTo(avatar.bottom)
}
)
// 副标题:标题下方
Text(
text = "声明式 UI 框架",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.constrainAs(subtitle) {
start.linkTo(title.start)
top.linkTo(avatar.bottom, margin = 8.dp)
}
)
}
}
逐步拆解:
createRefs():创建引用,数量和子组件一一对应constrainAs(ref) { }:把约束绑定到某个组件上start.linkTo(parent.start, margin = 16.dp):组件的左边,对齐到父组件的左边,间距 16dptop.linkTo(avatar.bottom):组件的顶部,对齐到头像的底部
linkTo就是 XML 里app:layout_constraintStart_toStartOf的 Compose 写法。左边约束到谁、上边约束到谁——思路完全一样。
4.4 约束速查表
| XML 属性 | Compose 写法 |
|---|---|
app:layout_constraintStart_toStartOf | start.linkTo(ref.start) |
app:layout_constraintEnd_toEndOf | end.linkTo(ref.end) |
app:layout_constraintTop_toTopOf | top.linkTo(ref.top) |
app:layout_constraintBottom_toBottomOf | bottom.linkTo(ref.bottom) |
app:layout_constraintStart_toEndOf | start.linkTo(ref.end) |
android:layout_marginStart | start.linkTo(ref.start, margin = 8.dp) |
app:layout_constraintHorizontal_bias | horizontalBias = 0.3f |
app:layout_constraintVertical_bias | verticalBias = 0.7f |
app:layout_constraintWidth_percent | width = Dimension.percent(0.5f) |
app:layout_constraintWidth_matchConstraint | width = Dimension.fillToConstraints |
app:layout_constraintWidth_wrapContent | width = Dimension.wrapContent |
4.5 Barrier 和 Guideline
这两个是 ConstraintLayout 的"辅助线",帮你处理更复杂的对齐场景。
Guideline(参考线):在布局中画一条虚拟的线,组件可以约束到这条线上:
@Composable
fun GuidelineDemo() {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (leftText, rightText) = createRefs()
val halfGuideline = createGuidelineFromStart(fraction = 0.5f)
Text(
text = "左半区",
modifier = Modifier.constrainAs(leftText) {
end.linkTo(halfGuideline, margin = 8.dp)
top.linkTo(parent.top)
}
)
Text(
text = "右半区",
modifier = Modifier.constrainAs(rightText) {
start.linkTo(halfGuideline, margin = 8.dp)
top.linkTo(parent.top)
}
)
}
}
Barrier(屏障):根据一组组件的边界动态生成一条线。比如两个文字长度不固定,你希望右边按钮始终在"两个文字中更长的那个"的右侧:
@Composable
fun BarrierDemo() {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (nameLabel, descLabel, valueText) = createRefs()
val endBarrier = createEndBarrier(nameLabel, descLabel)
Text(
text = "用户名",
modifier = Modifier.constrainAs(nameLabel) {
start.linkTo(parent.start, margin = 16.dp)
top.linkTo(parent.top, margin = 16.dp)
}
)
Text(
text = "个人描述(可能很长)",
modifier = Modifier.constrainAs(descLabel) {
start.linkTo(parent.start, margin = 16.dp)
top.linkTo(nameLabel.bottom, margin = 8.dp)
}
)
Text(
text = "值",
modifier = Modifier.constrainAs(valueText) {
start.linkTo(endBarrier, margin = 16.dp)
top.linkTo(parent.top, margin = 16.dp)
}
)
}
}
createEndBarrier(nameLabel, descLabel)创建一条"右边界屏障"——它始终在 nameLabel 和 descLabel 中靠右更远的那个的右侧。当 descLabel 比 nameLabel 宽时,屏障自动跟着右移。
4.6 ConstraintSet:解耦约束与组件
当约束很复杂时,把约束定义和 UI 组件分开写会更清晰。ConstraintSet 就是干这个的:
private fun profileConstraintSet(): ConstraintSet {
return ConstraintSet {
val avatar = createRefFor("avatar")
val name = createRefFor("name")
val desc = createRefFor("desc")
val divider = createRefFor("divider")
constrain(avatar) {
start.linkTo(parent.start, margin = 16.dp)
top.linkTo(parent.top, margin = 16.dp)
}
constrain(name) {
start.linkTo(avatar.end, margin = 12.dp)
top.linkTo(avatar.top)
end.linkTo(parent.end, margin = 16.dp)
width = Dimension.fillToConstraints
}
constrain(desc) {
start.linkTo(name.start)
top.linkTo(name.bottom, margin = 4.dp)
end.linkTo(parent.end, margin = 16.dp)
width = Dimension.fillToConstraints
}
constrain(divider) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(avatar.bottom, margin = 16.dp)
width = Dimension.fillToConstraints
}
}
}
@Composable
fun ConstraintSetDemo() {
ConstraintLayout(
constraintSet = profileConstraintSet(),
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier
.size(48.dp)
.layoutId("avatar")
)
Text(
text = "张三",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.layoutId("name")
)
Text(
text = "Android 开发者",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.layoutId("desc")
)
HorizontalDivider(modifier = Modifier.layoutId("divider"))
}
}
这段代码里发生了什么:
ConstraintSet里用createRefFor("avatar")创建字符串引用,不用createRefs()- 组件用
Modifier.layoutId("avatar")和约束集里的引用匹配 - 约束逻辑和 UI 组件完全分离——可以独立修改约束而不动组件代码
ConstraintSet的最大价值:约束可以动态切换。比如横屏和竖屏用不同的ConstraintSet,组件代码完全不变,只换约束。
五、自定义 Layout vs ConstraintLayout:怎么选
| 场景 | 推荐 | 原因 |
|---|---|---|
| 标准 UI 排版(表单、列表、卡片) | Column/Row/Box | 最简单,够用 |
| 多组件相对定位、避免深层嵌套 | ConstraintLayout | 扁平化,约束表达清晰 |
| 标准布局无法满足的特殊排列 | 自定义 Layout | 完全控制测量和放置 |
| 对单个组件做测量/位置微调 | Modifier.layout | 轻量,不引入新容器 |
| 需要根据子组件尺寸动态决定布局 | 自定义 Layout | ConstraintLayout 无法在测量阶段做计算 |
flowchart TD
A[需要自定义布局?] --> B{怎么排?}
B -->|标准排列| C[Column / Row / Box]
B -->|相对定位| D[ConstraintLayout]
B -->|特殊排列| E[自定义 Layout]
B -->|单组件微调| F[Modifier.layout]
B -->|动态计算| G[自定义 Layout]
不要为了用 ConstraintLayout 而用——3 个组件以内用 Column/Row 更清晰。ConstraintLayout 的优势在于组件多、约束复杂时减少嵌套。
六、综合实战:个人主页
这个 Demo 把前面所有知识点串起来——自定义 Layout、Modifier.layout、ConstraintLayout(含 Barrier 和 Guideline),一个不落。
// ---------- 自定义布局:标签流式换行 ----------
// 注意:Compose 1.4+ 内置了 FlowRow,这里自己实现是为了理解 Layout 原理
@Composable
private fun CustomFlowRow(
modifier: Modifier = Modifier,
horizontalGap: Dp = 8.dp,
verticalGap: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val hGapPx = horizontalGap.roundToPx()
val vGapPx = verticalGap.roundToPx()
val placeables = measurables.map { it.measure(constraints) }
var currentRowWidth = 0
var currentRowHeight = 0
var totalHeight = 0
val rows = mutableListOf<List<Placeable>>()
val currentRow = mutableListOf<Placeable>()
for (placeable in placeables) {
if (currentRowWidth + placeable.width > constraints.maxWidth && currentRow.isNotEmpty()) {
rows.add(currentRow.toList())
totalHeight += currentRowHeight + vGapPx
currentRow.clear()
currentRowWidth = 0
currentRowHeight = 0
}
currentRow.add(placeable)
currentRowWidth += placeable.width + hGapPx
currentRowHeight = maxOf(currentRowHeight, placeable.height)
}
if (currentRow.isNotEmpty()) {
rows.add(currentRow.toList())
totalHeight += currentRowHeight
}
layout(constraints.maxWidth, totalHeight) {
var y = 0
for (row in rows) {
var x = 0
var rowHeight = 0
for (placeable in row) {
placeable.placeRelative(x, y)
x += placeable.width + hGapPx
rowHeight = maxOf(rowHeight, placeable.height)
}
y += rowHeight + vGapPx
}
}
}
}
// ---------- 主页面 ----------
@Composable
fun ProfilePageDemo() {
val tags = listOf("Kotlin", "Compose", "Android", "MVVM", "Coroutines", "Retrofit")
Column(modifier = Modifier.fillMaxSize()) {
// 顶部封面区域:ConstraintLayout + Guideline
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
) {
val (coverBg, avatar, nameText, roleText) = createRefs()
val bottomGuideline = createGuidelineFromBottom(fraction = 0.35f)
Box(
modifier = Modifier
.constrainAs(coverBg) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(bottomGuideline)
}
.background(
Brush.verticalGradient(
colors = listOf(
Color(0xFF6200EE),
Color(0xFFBB86FC)
)
)
)
)
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.border(2.dp, Color.White, CircleShape)
.constrainAs(avatar) {
start.linkTo(parent.start, margin = 24.dp)
bottom.linkTo(bottomGuideline)
}
)
Text(
text = "张三",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.constrainAs(nameText) {
start.linkTo(avatar.end, margin = 16.dp)
bottom.linkTo(avatar.bottom, margin = 24.dp)
}
)
Text(
text = "Android 开发者",
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.8f),
modifier = Modifier.constrainAs(roleText) {
start.linkTo(nameText.start)
top.linkTo(nameText.bottom, margin = 2.dp)
}
)
}
// 信息卡片区域:ConstraintLayout + Barrier
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
val (label1, value1, label2, value2, label3, value3) = createRefs()
val valueBarrier = createEndBarrier(label1, label2, label3)
Text(
text = "城市",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.constrainAs(label1) {
start.linkTo(parent.start)
top.linkTo(parent.top)
}
)
Text(
text = "北京",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.constrainAs(value1) {
start.linkTo(valueBarrier, margin = 24.dp)
top.linkTo(label1.top)
}
)
Text(
text = "经验",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.constrainAs(label2) {
start.linkTo(parent.start)
top.linkTo(label1.bottom, margin = 8.dp)
}
)
Text(
text = "5 年",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.constrainAs(value2) {
start.linkTo(valueBarrier, margin = 24.dp)
top.linkTo(label2.top)
}
)
Text(
text = "技术栈",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.constrainAs(label3) {
start.linkTo(parent.start)
top.linkTo(label2.bottom, margin = 8.dp)
}
)
Text(
text = "Kotlin / Compose",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.constrainAs(value3) {
start.linkTo(valueBarrier, margin = 24.dp)
top.linkTo(label3.top)
}
)
}
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
// 技能标签区域:自定义 FlowRow
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "技能标签",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
CustomFlowRow(horizontalGap = 8.dp, verticalGap = 8.dp) {
tags.forEach { tag ->
SuggestionChip(
onClick = { },
label = { Text(tag, style = MaterialTheme.typography.bodySmall) }
)
}
}
}
// 简介卡片:Modifier.layout 实现自定义内边距
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.customPadding(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(
text = "5 年 Android 开发经验,专注 Jetpack Compose 和现代 Android 开发。" +
"热爱开源,喜欢用 Kotlin 解决实际问题。",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(12.dp)
)
}
}
}
实战知识点对应表
| 实战中的效果 | 使用的 API | 对应章节 |
|---|---|---|
| 标签流式换行排列 | 自定义 Layout(CustomFlowRow) | 二 |
| 信息标签右侧对齐 | ConstraintLayout + Barrier | 四 |
| 封面区域定位 | ConstraintLayout + Guideline | 四 |
| 简介卡片自定义内边距 | Modifier.layout(customPadding) | 三 |
七、总结
本篇你学到了 Compose 自定义布局的核心能力:
- 布局原理:先测量(measure)、再放置(place),和 XML 的
onMeasure/onLayout一脉相承 LayoutComposable:创建自定义布局容器,完全控制子组件的测量和位置Modifier.layout:对单个组件做测量/位置微调,类似自定义 LayoutParamsConstraintLayout:扁平化复杂布局,用约束替代嵌套;Barrier和Guideline处理动态对齐ConstraintSet:约束与组件解耦,便于动态切换约束- 选型指南:简单用 Column/Row、复杂用 ConstraintLayout、特殊用自定义 Layout
核心原则:能用标准布局就用标准布局,遇到标准布局搞不定的场景再上自定义 Layout 或 ConstraintLayout。不要为了炫技而把简单问题复杂化。
下一篇我们将学习 Navigation 导航——如何在多个页面之间跳转、传参、管理返回栈,把单页应用变成多页应用。
如果你在学习过程中有任何疑问,欢迎在评论区留言,我会尽可能回复。
系列文章:
- Jetpack Compose 入门系列(一):从零搭建到基础控件使用
- Jetpack Compose 入门系列(二):布局到 State 的使用
- Jetpack Compose 入门系列(三):MVVM + 协程 实现网络请求列表
- Jetpack Compose 入门系列(四):动画基本使用
- Jetpack Compose 入门系列(五):自定义布局与 ConstraintLayout(本文)