本课定位: 第 1 课建立了状态驱动的心智模型,第 2 课掌握了空间组织能力。第 3 课进入 Compose 的"外观系统"——Modifier。Modifier 是 Compose 中使用频率最高的 API,它决定组件的尺寸、内边距、背景、边框、圆角、阴影、点击、滚动、变换等几乎所有视觉和交互属性。理解 Modifier 顺序,是区分"会用 Compose"和"用得好 Compose"的分水岭。 后面主题、动画、手势、性能优化,都建立在本课之上。
一、本课目标
学完本课,你应该能够:
二、Modifier 的本质
2.1 什么是 Modifier?
Modifier 是 Compose 中用来"修饰组件"的对象。它可以设置尺寸、内边距、背景、边框、圆角、阴影、点击、滚动等属性。
Text(
text = "你好",
modifier = Modifier
.padding(16.dp)
.background(Color.Yellow)
.clickable { }
)
这里 Modifier 链按顺序作用在 Text 上。注意:Modifier 不是 XML 属性,它是一段可组合的行为描述。 每个修饰符都会改变最终组件的测量、布局、绘制或交互行为。
2.2 Modifier 的三个核心特性
特性一:链式调用
Modifier
.padding(16.dp)
.background(Color.Yellow)
.clickable { }
每个方法返回一个新的 Modifier,可以继续链式调用。
特性二:不可变
Modifier 本身是不可变的。Modifier.padding(16.dp) 不会修改原来的 Modifier,而是返回一个新的 Modifier。这意味着你可以安全地复用和共享 Modifier。
val baseModifier = Modifier.padding(16.dp)
val cardModifier = baseModifier.background(Color.White) // baseModifier 不变
val chipModifier = baseModifier.background(Color.Yellow) // 各自独立
这在 Compose 中非常重要。如果 Modifier 可变,共享会导致难以排查的样式污染。
特性三:顺序敏感
Modifier.padding(16.dp).background(Color.Red)
和
Modifier.background(Color.Red).padding(16.dp)
效果完全不同。顺序是 Modifier 最重要的语义。
2.3 Modifier 到底做了什么?
可以把 Modifier 想象成"一层一层包裹组件的包装纸":
Modifier
.padding(16.dp) // 第 1 层:外面加 16dp
.background(Red) // 第 2 层:加红色背景
.size(80.dp) // 第 3 层:固定 80dp
每一层都会影响下一层的可用空间和行为。
更本质地说,Modifier 会参与三个阶段:
- 测量阶段:影响约束,比如 size、padding、fillMaxWidth。
- 绘制阶段:影响外观,比如 background、border、shadow。
- 交互阶段:影响事件,比如 clickable、pointerInput。
理解这一点,就能理解为什么 Modifier 顺序会影响测量和绘制。
2.4 从"包装纸"到"洋葱模型"
把 Modifier 链想象成一个洋葱:
┌─────────────────────────────┐
│ padding(16.dp) │ ← 最外层
│ ┌───────────────────────┐ │
│ │ background(Red) │ │
│ │ ┌─────────────────┐ │ │
│ │ │ size(80.dp) │ │ │
│ │ │ ┌───────────┐ │ │ │
│ │ │ │ 内容 │ │ │ │
│ │ │ └───────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
- 越靠外的修饰符,作用范围越大。
- 越靠内的修饰符,作用范围越小。
- 每一层都缩小或改变下一层的可用空间。
这个模型能解释几乎所有 Modifier 顺序问题。
2.5 Modifier 与 Modifier.companion
Modifier.padding(16.dp)
Modifier 本身是一个伴生对象,相当于"空的 Modifier 起点"。
val base = Modifier
val padded = base.padding(16.dp)
base 是空修饰符,padded 是新的修饰符。不要修改已有的 Modifier,永远返回新的。
2.6 为什么 Composable 推荐接收 modifier: Modifier = Modifier
@Composable
fun MyCard(
modifier: Modifier = Modifier
) {
Column(modifier = modifier) { ... }
}
这样做的好处:
注意:组件内部应该把 modifier 应用在最外层,而不是中间层。否则外部传入的修饰符可能被内部修饰符覆盖。
// 正确:外部 Modifier 应用在最外层
@Composable
fun MyCard(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) { ... }
}
// 错误:外部 Modifier 被内部 padding 限制
@Composable
fun MyCard(modifier: Modifier = Modifier) {
Column(modifier = Modifier.padding(16.dp).then(modifier)) { ... }
}
2.7 then:组合两个 Modifier
Modifier
.padding(16.dp)
.then(otherModifier)
then 用于把两个 Modifier 组合起来。常见场景:
@Composable
fun MyCard(modifier: Modifier = Modifier) {
val baseModifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
Column(modifier = modifier.then(baseModifier)) { ... }
}
then 的顺序同样重要:先写的靠外,后写的靠内。
三、尺寸修饰符
3.1 size:固定宽高
Modifier.size(80.dp)
Modifier.size(width = 80.dp, height = 40.dp)
size 会同时设置宽度和高度。
3.2 width / height:单独设置
Modifier.width(120.dp)
Modifier.height(48.dp)
3.3 fillMaxWidth / fillMaxHeight / fillMaxSize
Modifier.fillMaxWidth() // 宽度占满父容器允许的最大宽度
Modifier.fillMaxHeight() // 高度占满父容器允许的最大高度
Modifier.fillMaxSize() // 宽高都占满
Modifier.fillMaxWidth(0.5f) // 占父容器宽度的 50%
深入理解:fillMaxWidth(fraction) 允许按比例填充。fraction = 0.5f 表示占一半。fraction 的范围是 0f 到 1f。
3.4 wrapContentSize:包裹内容
Modifier.wrapContentSize()
Modifier.wrapContentWidth()
Modifier.wrapContentHeight()
这是默认行为。除非特别需要,通常不需要显式写。
3.5 requiredSize:无视父约束
Modifier.requiredSize(120.dp)
requiredSize 会忽略父容器的约束,强制使用指定尺寸。慎用,因为它会破坏父容器的布局。常见场景:图片必须显示某个固定尺寸时。
// 父容器只给了 100dp
Row(modifier = Modifier.width(100.dp)) {
// 子元素强制 200dp,会溢出
Box(modifier = Modifier.requiredSize(200.dp))
}
3.6 sizeIn:约束范围
Modifier.sizeIn(minWidth = 100.dp, maxWidth = 200.dp)
Modifier.sizeIn(minHeight = 40.dp)
Modifier.sizeIn(minWidth = 100.dp, minHeight = 40.dp)
sizeIn 不是固定尺寸,而是给尺寸设置上下限。
3.7 defaultMinSize:最小尺寸
Modifier.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
defaultMinSize 设置默认最小尺寸,但如果内容更大,会使用内容尺寸。和 sizeIn 的区别:
- sizeIn:约束最终尺寸。
- defaultMinSize:只在内容小于最小值时生效。
3.8 aspectRatio:宽高比
Modifier.aspectRatio(16f / 9f)
Modifier.aspectRatio(1f) // 正方形
aspectRatio 让组件保持固定宽高比。常用于图片、视频封面、方形头像。
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.background(Color.Gray)
)
3.9 尺寸修饰符对比
| size | 固定尺寸 | 受 |
| width / height | 单独固定 | 受 |
| fillMaxWidth | 占满允许宽度 | 受 |
| fillMaxWidth(0.5f) | 按比例占满 | 受 |
| wrapContentSize | 包裹内容 | 受 |
| requiredSize | 强制尺寸 | 不受 |
| sizeIn | 尺寸范围 | 受 |
| defaultMinSize | 默认最小尺寸 | 受 |
| aspectRatio | 固定宽高比 | 受 |
四、内外边距:padding
4.1 padding 的用法
Modifier.padding(16.dp) // 四周 16dp
Modifier.padding(horizontal = 16.dp) // 左右 16dp
Modifier.padding(vertical = 8.dp) // 上下 8dp
Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
4.2 padding 的本质是"内边距"
padding 不是"外边距"(margin)。在 Compose 中,padding 是通过缩小内容的可用空间来实现的。
Text(
text = "你好",
modifier = Modifier
.background(Color.Yellow)
.padding(16.dp)
)
效果:背景覆盖整个区域,包括 padding 部分。因为 background 在外层,padding 在内层。
Text(
text = "你好",
modifier = Modifier
.padding(16.dp)
.background(Color.Yellow)
)
效果:先留出 16dp 外边距,背景只画在内容区域。
4.3 Compose 没有 margin,怎么实现外边距?
在 Compose 中,"外边距"通常由父容器控制子元素间距或子元素自己加 padding 实现:
- 父容器用 Arrangement.spacedBy(12.dp)。
- 父容器用 Spacer。
- 子元素用 Modifier.padding(…)。
理解这一点,就能从"margin/padding"的传统思维切换到"Compose 的修饰符链"思维。
4.4 padding 与 size 的顺序
Modifier
.size(80.dp)
.padding(16.dp)
.background(Color.Red)
效果:整体 80dp,内部 padding 16dp,背景画在 padding 内层。实际红色区域是 80 – 32 = 48dp。
Modifier
.padding(16.dp)
.size(80.dp)
.background(Color.Red)
效果:外层 padding 16dp,再固定 80dp,背景画在 80dp 区域内。整体占用 80 + 32 = 112dp。
尺寸修饰符和内边距的顺序会影响最终占用空间。
4.5 一个常见需求:内边距不影响背景
如果希望背景覆盖整体,但内容有内边距:
Modifier
.background(Color.Red)
.padding(16.dp)
如果希望背景只画在内容区域:
Modifier
.padding(16.dp)
.background(Color.Red)
选择哪种,取决于你想要的视觉效果。
五、背景与边框
5.1 background
Modifier.background(Color.Red)
Modifier.background(Color.Red, shape = RoundedCornerShape(8.dp))
Modifier.background(color = Color.Red, alpha = 0.5f)
Modifier.background(Brush.horizontalGradient(listOf(Color.Red, Color.Blue)))
background 支持颜色、形状、透明度,还支持 Brush 渐变:
Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6200EE), Color(0xFF03DAC5))
),
shape = RoundedCornerShape(12.dp)
)
常用写法:
Modifier.background(
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.medium
)
5.2 border
Modifier.border(1.dp, Color.Gray)
Modifier.border(2.dp, Color.Red, shape = RoundedCornerShape(8.dp))
Modifier.border(BorderStroke(2.dp, Color.Red), shape = CircleShape)
Modifier.border(
border = BorderStroke(1.dp, Brush.horizontalGradient(...)),
shape = RoundedCornerShape(8.dp)
)
border 需要指定宽度、颜色、形状。注意:
- border 是描边,不是背景。
- border 必须配合 shape 才能画出圆角边框。
- border 不会裁剪内容,只画边框。
- border 支持 Brush,可以做渐变边框。
5.3 background 与 border 的顺序
Modifier
.background(Color.Red)
.border(2.dp, Color.Blue, RoundedCornerShape(8.dp))
效果:背景先画,边框画在背景上面。边框不会被背景遮住。
Modifier
.border(2.dp, Color.Blue, RoundedCornerShape(8.dp))
.background(Color.Red)
效果:边框先画,背景再画在边框下面。如果背景是实心的,边框可能被覆盖。
推荐顺序:
Modifier
.clip(shape) // 先裁剪
.background(color) // 再画背景
.border(width, color, shape) // 最后画边框
5.4 border 与 clip 的配合
如果想让内容也被圆角裁剪,需要 clip:
Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.border(1.dp, Color.Gray, RoundedCornerShape(12.dp))
clip 负责裁剪内容和背景,border 负责描边。两者必须使用相同的 shape,才能视觉统一。
5.5 border 的进阶:BorderStroke
BorderStroke(1.dp, Color.Gray)
BorderStroke(1.dp, Brush.horizontalGradient(listOf(Color.Red, Color.Blue)))
BorderStroke 是边框的完整描述,支持渐变。使用场景:
- 选中态边框。
- 渐变边框。
- 虚线边框(需要自定义 PathEffect)。
六、圆角与裁剪:clip
6.1 clip 的作用
clip 把组件内容裁剪成指定形状。
Modifier.clip(CircleShape)
Modifier.clip(RoundedCornerShape(12.dp))
Modifier.clip(RoundedCornerShape(topStart = 12.dp, bottomEnd = 12.dp))
6.2 常用形状
| CircleShape | 圆形 |
| RoundedCornerShape(8.dp) | 8dp 圆角矩形 |
| RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) | 只有上方圆角 |
| CutCornerShape(8.dp) | 切角 |
| MaterialTheme.shapes.small/medium/large | 主题形状 |
6.3 clip 的位置非常关键
Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.Blue)
效果:先确定 80dp 尺寸,再裁剪成圆形,再画背景,得到圆形头像。
Modifier
.size(80.dp)
.background(Color.Blue)
.clip(CircleShape)
效果:先确定尺寸,再画背景,最后裁剪。如果背景色已经铺满,裁剪后仍是圆形,但某些场景下视觉效果可能不同。
推荐顺序:
size -> clip -> background -> border
6.4 clip 与 border 的常见坑
Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color.White)
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
注意:border 需要自己指定 shape,它不会自动使用 clip 的形状。如果想让边框和裁剪一致,要传入相同的形状。
6.5 clip 与 clickable 的配合
如果希望点击区域是圆角形状:
Modifier
.clip(RoundedCornerShape(12.dp))
.clickable { }
.background(Color.White)
clip 在前,clickable 在后,水波纹会沿圆角范围扩散。
6.6 clipToBounds:裁剪越界内容
Modifier.clipToBounds()
clipToBounds 把内容裁剪到组件边界。常见场景:
- 子元素超出父容器时,裁剪掉越界部分。
- 滚动列表中的图片。
七、阴影与层级
7.1 shadow
Modifier.shadow(elevation = 4.dp)
Modifier.shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp))
Modifier.shadow(elevation = 8.dp, shape = CircleShape, clip = false)
参数:
- elevation:阴影大小。
- shape:阴影形状。
- clip:是否裁剪内容。默认 false。
- ambientColor、spotColor:阴影颜色。
7.2 shadow 的顺序
shadow 必须在 background 之前,才能看到阴影:
Modifier
.shadow(4.dp, RoundedCornerShape(12.dp))
.background(Color.White)
如果顺序错误,阴影可能被背景遮住。
7.3 zIndex
zIndex 控制层叠顺序:
Box {
Box(modifier = Modifier.zIndex(1f)) { ... }
Box(modifier = Modifier.zIndex(2f)) { ... }
}
zIndex 数值越大越靠上。常用于自定义层叠效果。
7.4 Card 与 Surface 的阴影
Card 自带阴影:
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ... }
Surface 也支持 shadowElevation 和 tonalElevation:
Surface(
tonalElevation = 2.dp,
shadowElevation = 4.dp
) { ... }
在真实项目中,优先用 Card 或 Surface 做卡片,而不是手写 Modifier.shadow。
7.5 tonalElevation 和 shadowElevation 的区别
| tonalElevation | 通过色调变化表达高度,不改阴影 |
| shadowElevation | 通过阴影表达高度 |
Material 3 中,tonalElevation 用于浅色表面,shadowElevation 用于需要明显阴影的场景。
八、点击与交互
8.1 clickable
Modifier.clickable { /* 点击 */ }
Modifier.clickable(enabled = true) { /* 点击 */ }
Modifier.clickable(
enabled = true,
onClickLabel = "点击卡片"
) { /* 点击 */ }
clickable 会自动添加水波纹(Ripple)效果,前提是外层有 MaterialTheme。
onClickLabel 用于无障碍服务,描述点击行为。
8.2 combinedClickable
@OptIn(ExperimentalFoundationApi::class)
Modifier.combinedClickable(
onClick = { /* 单击 */ },
onLongClick = { /* 长按 */ },
onDoubleClick = { /* 双击 */ }
)
combinedClickable 是实验性 API,需要 @OptIn(ExperimentalFoundationApi::class)。
8.3 clickable 与 padding 的顺序
Modifier
.clickable { }
.padding(16.dp)
点击范围包括 padding 部分。
Modifier
.padding(16.dp)
.clickable { }
点击范围只有内容区域,padding 部分不响应点击。
推荐: 如果希望整块卡片可点击,把 clickable 放在 padding 之前。
8.4 clip 与 clickable 的配合
如果希望点击区域是圆角形状:
Modifier
.clip(RoundedCornerShape(12.dp))
.clickable { }
.background(Color.White)
clip 在前,clickable 在后,水波纹会沿圆角范围扩散。
8.5 用 Surface 代替 clickable + background
Surface(
onClick = { /* 点击 */ },
shape = RoundedCornerShape(12.dp),
color = Color.White
) { ... }
Surface 自带了点击、背景、形状、阴影,代码更简洁,推荐在卡片场景使用。
8.6 indication 和 interactionSource
val interactionSource = remember { MutableInteractionSource() }
Modifier.clickable(
interactionSource = interactionSource,
indication = null // 去掉水波纹
) { }
indication = null 可以去掉水波纹。常见场景:自定义点击反馈。
九、Card、Surface、Button 样式定制
9.1 Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
// 内容
}
Card 的常用参数:
- shape:形状。
- colors:背景色和内容色。
- elevation:阴影。
- border:边框。
9.2 Surface
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
tonalElevation = 2.dp,
shadowElevation = 4.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
) {
// 内容
}
Surface 的常用参数:
- shape:形状。
- color:背景色。
- contentColor:内容色。
- tonalElevation:色调高度。
- shadowElevation:阴影高度。
- border:边框。
- onClick:点击。
9.3 Button
Button(
onClick = { },
shape = RoundedCornerShape(24.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
) {
Text("按钮")
}
9.4 选择建议
| 普通卡片 | Card |
| 带点击的卡片 | Surface(onClick = …) |
| 自定义背景 + 边框 | Surface |
| 主按钮 | Button |
| 次按钮 | OutlinedButton |
| 文字按钮 | TextButton |
| 图标按钮 | IconButton |
| 浮动操作按钮 | FloatingActionButton |
9.5 按钮尺寸定制
Button(
onClick = { },
modifier = Modifier.height(48.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp)
) {
Text("按钮")
}
contentPadding 控制按钮内容的内边距,比 Modifier.padding 更推荐。
十、完整示例:卡片样式组件库
下面做一个"卡片样式组件库",包含普通卡片、可点击卡片、标签、头像、按钮。
package com.example.composemodifier
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
StyleShowcaseScreen()
}
}
}
}
}
@Composable
fun StyleShowcaseScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
SimpleCard()
ClickableCard()
TagRow()
ButtonRow()
}
}
@Composable
fun SimpleCard(modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("普通卡片", style = MaterialTheme.typography.titleMedium)
Text(
"使用 Card + padding + elevation 实现。",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun ClickableCard(modifier: Modifier = Modifier) {
Surface(
onClick = { },
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
shadowElevation = 2.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Avatar("李")
Column(modifier = Modifier.weight(1f)) {
Text("可点击卡片", style = MaterialTheme.typography.titleMedium)
Text(
"点击整块区域触发 onClick。",
style = MaterialTheme.typography.bodySmall
)
}
Text(">", style = MaterialTheme.typography.titleMedium)
}
}
}
@Composable
fun TagRow(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Tag("Kotlin")
Tag("Compose")
Tag("Modifier")
}
}
@Composable
private fun Tag(text: String, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.primaryContainer
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@Composable
fun ButtonRow(modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = { },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(24.dp)
) {
Text("主按钮")
}
OutlinedButton(
onClick = { },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(24.dp)
) {
Text("次按钮")
}
}
}
@Composable
fun Avatar(name: String, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = name.take(1),
color = Color.White,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
@Preview(showBackground = true, name = "样式展示")
@Composable
fun StyleShowcasePreview() {
MaterialTheme {
StyleShowcaseScreen()
}
}
10.1 用到的修饰符
| 卡片背景与圆角 | Card 自带 shape、elevation |
| 可点击卡片 | Surface(onClick = …) |
| 圆角标签 | Surface(shape = RoundedCornerShape(50)) |
| 圆形头像 | size -> clip(CircleShape) -> background |
| 按钮圆角 | Button(shape = RoundedCornerShape(24.dp)) |
| 按钮等宽 | Modifier.weight(1f) |
10.2 关键设计点
- 圆角统一用 RoundedCornerShape,卡片 12dp,标签全圆角,按钮 24dp。
- 卡片优先用 Card 或 Surface,不手写 shadow。
- 可点击卡片优先用 Surface(onClick = …),自带水波纹和点击处理。
- 圆形头像用 size -> clip -> background 顺序。
10.3 代码复用技巧
- Avatar、Tag、ButtonRow 都是独立组件,可在其他页面复用。
- 每个组件都接收 modifier: Modifier = Modifier,外部可控。
- 颜色统一用 MaterialTheme.colorScheme,便于后续主题切换。
十一、随堂练
十二、15 道习题(含参考答案与解读)
习题 1:Modifier 的三个核心特性是什么?
参考答案: 链式调用、不可变、顺序敏感。
解读: 链式调用让修饰符可以连续叠加;不可变意味着每次调用返回新 Modifier,不会修改原对象;顺序敏感意味着不同顺序会产生不同效果。这三点是理解 Modifier 的基础。特别是顺序敏感,是 Compose 与 XML 属性最大的差别之一。XML 属性是无序的,而 Modifier 是有序的、有语义的。
习题 2:Modifier.padding(16.dp) 和 Modifier.background(Color.Red) 顺序不同,效果有什么区别?
参考答案:
- padding 在前:先留出 16dp 外边距,背景只画在内容区域。
- background 在前:背景覆盖整个区域,包括 padding 部分。
解读: 这体现 Modifier 链的"包裹"语义。先写的修饰符更靠外,后写的更靠内。padding 缩小内容区域,background 画在它所在的层。理解这一点,就能控制背景、点击、边框的范围。可以用洋葱模型记忆:
外层 = 先写
内层 = 后写
习题 3:Compose 有 margin 吗?如何实现外边距?
参考答案: Compose 没有 margin。外边距通常通过父容器控制间距(Arrangement.spacedBy、Spacer)或子元素自己加 padding 实现。
解读: 这与 Compose 的"修饰符链"思维一致:没有独立的 margin 属性,间距由父容器或自身修饰符承担。在 XML 中习惯用 margin 的开发者,需要适应这种思维。一个常见做法:
// 父容器控制间距
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Item()
Item()
}
这比给每个 Item 加 margin 更统一、更可维护。
习题 4:fillMaxWidth() 和 requiredSize() 有什么区别?
参考答案: fillMaxWidth() 占满父容器允许的最大宽度,仍受父约束限制;requiredSize() 强制使用指定尺寸,忽略父约束。
解读: requiredSize 是一把"双刃剑"。它能强制尺寸,但会破坏父容器的布局。比如父容器只给了 100dp,子元素 requiredSize(200.dp) 会溢出,可能被裁剪。除非确实需要,否则优先用 size 或 fillMaxWidth。
// 慎用
Modifier.requiredSize(200.dp)
// 推荐
Modifier.fillMaxWidth()
Modifier.sizeIn(minWidth = 100.dp, maxWidth = 200.dp)
习题 5:Modifier.size(80.dp) 和 Modifier.sizeIn(minWidth = 80.dp, maxWidth = 120.dp) 有什么区别?
参考答案: size 固定 80dp;sizeIn 把尺寸限制在 80~120dp 之间,具体尺寸仍由内容决定。
解读: sizeIn 适合"尺寸可变但有范围"的场景,比如自适应标签、输入框。它不会强制尺寸,只约束范围,比 size 更灵活。
// 固定尺寸
Modifier.size(80.dp)
// 范围约束
Modifier.sizeIn(minWidth = 80.dp, maxWidth = 120.dp)
习题 6:写出一个圆形头像的 Modifier 链。
参考答案:
Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.Blue)
解读: 顺序是 size -> clip -> background。先确定尺寸,再裁剪成圆形,再画背景,才能得到圆形。如果 background 在 clip 之前,背景不会被裁剪,可能显示为方形。
为什么这个顺序最稳妥?因为:
习题 7:clip 和 border 配合时有什么坑?
参考答案: border 不会自动使用 clip 的形状,需要自己传入 shape。
解读: 常见写法:
Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.border(1.dp, Color.Gray, RoundedCornerShape(12.dp))
border 的 shape 要和 clip 一致,才能视觉统一。如果 border 不传 shape,会画成矩形边框,与圆角内容不匹配。可以提取一个变量复用:
val shape = RoundedCornerShape(12.dp)
Modifier
.clip(shape)
.background(Color.White)
.border(1.dp, Color.Gray, shape)
习题 8:shadow 应该在 background 之前还是之后?为什么?
参考答案: shadow 应该在 background 之前。
解读: shadow 需要在背景下面画阴影。如果 background 在前面,会覆盖阴影,导致阴影不可见。推荐顺序:
Modifier
.shadow(4.dp, RoundedCornerShape(12.dp))
.background(Color.White)
在实际项目中,优先用 Card 或 Surface 的 elevation,而不是手写 shadow。原因是官方组件更稳定,且能随主题变化。
习题 9:clickable 和 padding 顺序不同,点击范围有什么区别?
参考答案:
- clickable 在前:点击范围包括 padding 部分。
- padding 在前:点击范围只有内容区域。
解读: 这和第 2 题同理。clickable 是一个"响应点击的层",它所在的层有多大,点击范围就有多大。如果希望整块卡片可点击,把 clickable 放在 padding 之前。
// 整块可点击
Modifier
.clip(shape)
.clickable { }
.background(color)
.padding(16.dp)
// 只有内容可点击
Modifier
.clip(shape)
.background(color)
.padding(16.dp)
.clickable { }
习题 10:Surface(onClick = …) 和 Modifier.clickable { } 有什么区别?
参考答案: Surface(onClick = …) 是组件级别的点击,自带背景、形状、阴影、水波纹;Modifier.clickable { } 是修饰符级别的点击,只负责响应点击和添加水波纹,背景、形状要自己加。
解读: 卡片场景推荐 Surface(onClick = …),代码更简洁、视觉更统一。自定义场景(比如只需要点击而不需要背景)用 clickable。Surface 还支持 tonalElevation、shadowElevation 等参数,样式更完整。
// 推荐:Surface
Surface(
onClick = { },
shape = RoundedCornerShape(12.dp),
color = Color.White,
shadowElevation = 4.dp
) { ... }
// 自定义:clickable
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.clickable { }
) { ... }
习题 11:Card 和 Surface 有什么区别?如何选择?
参考答案: Card 是 Material 3 封装的卡片组件,自带 shape、elevation、colors;Surface 是更底层的通用容器,支持 onClick、border、tonalElevation、shadowElevation。
解读:
- 普通卡片:Card。
- 带点击的卡片:Surface(onClick = …)。
- 需要自定义边框:Surface(border = …)。
- 需要色调高度:Surface(tonalElevation = …)。
- 需要同时控制阴影和色调:Surface。
Card 本质上也是基于 Surface 的封装,理解这一点就不会纠结。
习题 12:Button、OutlinedButton、TextButton 有什么区别?
参考答案:
- Button:实心主按钮。
- OutlinedButton:带边框的次按钮。
- TextButton:纯文字按钮。
解读: 三者都属于 Material 3 按钮体系,按重要性层级区分。主操作使用 Button,次要操作使用 OutlinedButton,辅助操作使用 TextButton。它们都支持 shape、colors、border 等参数。
Button(onClick = {}) { Text("主操作") }
OutlinedButton(onClick = {}) { Text("次操作") }
TextButton(onClick = {}) { Text("辅助操作") }
习题 13:如何让 Row 中两个按钮等宽?
参考答案:
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("主按钮")
}
OutlinedButton(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("次按钮")
}
}
解读: 关键在 Modifier.weight(1f)。weight 只能在 RowScope 中使用。Arrangement.spacedBy(12.dp) 负责按钮之间的间距,weight(1f) 负责等宽。两者配合,得到等宽且间距统一的按钮行。
习题 14:Card 的 shape、elevation、colors 分别控制什么?
参考答案:
- shape:卡片形状,控制圆角。
- elevation:卡片阴影高度。
- colors:卡片背景色和内容色。
解读:
Card(
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) { ... }
这三个参数是卡片样式的主要定制点。颜色建议使用 MaterialTheme.colorScheme,让卡片随主题变化。
习题 15:综合题:手写一个带圆角、阴影、边框、点击的卡片,不用 Card / Surface。
参考答案:
Box(
modifier = Modifier
.fillMaxWidth()
.shadow(4.dp, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.border(1.dp, Color.Gray, RoundedCornerShape(12.dp))
.clickable { }
.padding(16.dp)
) {
Column {
Text("手写卡片", style = MaterialTheme.typography.titleMedium)
Text("圆角、阴影、边框、点击都手动实现。")
}
}
解读: 顺序是:
shadow -> clip -> background -> border -> clickable -> padding
- shadow:在背景之前,阴影可见。
- clip:裁剪内容。
- background:画背景。
- border:描边,形状与 clip 一致。
- clickable:点击范围包括 padding。
- padding:内边距。
对比 Card 或 Surface,手写需要 6 个修饰符,代码量明显更多。实际项目中优先用 Card 或 Surface,但理解手写方式能帮你应对自定义场景。
逆向思维训练: 从效果反推顺序:
这就是"按效果推顺序"的能力。
十三、作业(含参考答案与解读)
作业 1(必做):完成"卡片样式组件库"
题目要求:
- 至少包含:普通卡片、可点击卡片、标签、圆形头像、按钮。
- 使用 Card、Surface、Modifier 实现。
- 至少一个 @Preview。
参考答案:
参考第十节完整示例。核心组件:
SimpleCard() // 普通卡片
ClickableCard() // 可点击卡片
TagRow() // 标签行
Avatar("李") // 圆形头像
ButtonRow() // 按钮行
解读:
- 普通卡片用 Card + padding + elevation。
- 可点击卡片用 Surface(onClick = …)。
- 标签用 Surface + RoundedCornerShape(50)。
- 头像用 size -> clip -> background。
- 按钮用 Button / OutlinedButton + weight。
作业 2(选做):手写一个带圆角、阴影、边框、点击的卡片
题目要求:
- 不使用 Card 或 Surface。
- 只用 Box 和 Modifier 实现。
参考答案:
参考习题 15。
解读:
- 顺序:shadow -> clip -> background -> border -> clickable -> padding。
- 与 Card 对比代码量,理解封装的价值。
- 实际项目中推荐 Card / Surface,但手写训练能加深对 Modifier 顺序的理解。
- 建议尝试调整顺序,观察效果变化,加深理解。
作业 3(提交):代码文件或 Git 仓库链接
题目要求:
- 提交代码文件或 Git 仓库链接。
参考答案:
项目结构建议:
ComposeModifier/
├── app/
│ └── src/main/java/com/example/composemodifier/
│ └── MainActivity.kt
└── README.md
解读:
- 提交前确保项目能编译、能运行、有 @Preview。
- 不要提交 build/、.gradle/ 等生成目录。
- README.md 写清楚功能、运行环境、截图。
作业 4(提交):运行截图
题目要求:
- 提交运行截图。
参考答案:
建议至少提交:
解读:
- 截图要能证明样式真实运行成功。
- 如果有水波纹,最好截出点击瞬间的效果。
- 建议对比"正确顺序"和"错误顺序"的截图,说明 Modifier 顺序的重要性。
作业 5(提交):200 字学习总结
题目要求:
- 写 200 字学习总结:Modifier 顺序、padding 与 background、clip 与 border、clickable 的位置。
参考答案:
本课让我理解了 Modifier 的三个核心特性:链式、不可变、顺序敏感。顺序不同,效果完全不同。比如 size -> clip -> background 才能得到圆形头像;background 在 padding 之前会覆盖 padding 区域。Compose 没有 margin,外边距要靠父容器或 padding 实现。clip 负责裁剪,border 负责描边,两者需要传入相同的 shape 才能视觉统一。shadow 要在 background 之前才能可见。clickable 的位置决定点击范围:在 padding 之前点击包括 padding,在 padding 之后点击只有内容区域。通过卡片样式组件库,我学会了用 Card、Surface、Button 定制样式,也手写了一个卡片理解封装的价值。后续我会继续学习状态与重组,让样式随状态变化。
解读:
- 总结中应出现关键词:Modifier 顺序、padding、background、clip、border、shadow、clickable、Card、Surface。
- 结合卡片样式组件库,说明样式定制的组织方式。
- 如果能写出自己踩过的坑,例如"border 忘记传 shape",会更有深度。
- 建议结合"洋葱模型"或"包裹语义"描述 Modifier 顺序。
十四、常见坑与调试建议
Modifier 顺序错误
背景范围、点击范围、圆角效果不符预期,先检查顺序。
border 忘记传 shape
圆角内容 + 矩形边框,视觉不统一。border 的 shape 要和 clip 一致。
shadow 被 background 覆盖
shadow 要在 background 之前。
padding 当成 margin 用
Compose 没有 margin,padding 是内边距。外边距需要父容器控制。
clickable 和 padding 顺序搞反
点击范围不符合预期时,检查 clickable 位置。
Card 和 Surface 混用
通常只需要其中一个。Card 更简单,Surface 更灵活。
fillMaxWidth 和 requiredSize 冲突
requiredSize 会忽略父约束,慎用。
clip 和 border 形状不一致
两者需要相同的 shape 才能视觉统一。建议提取成变量复用。
组件内部忘记应用 modifier
组件接收 modifier 参数后,要把它应用到最外层,否则外部无法控制间距和尺寸。
硬编码颜色
优先用 MaterialTheme.colorScheme,让样式随主题变化。
shadow 不显示
检查 elevation 是否大于 0,shape 是否匹配,是否被背景覆盖。
background 渐变不显示
检查 Brush 是否正确传入,shape 是否与 clip 一致。
clickable 无水波纹
检查外层是否有 MaterialTheme,或 indication 是否被设为 null。
Modifier 重复叠加
同一个修饰符叠加多次可能产生意外效果,尽量保持链简洁。
在 Composable 中创建 Modifier 导致频繁重建
简单场景没问题,但复杂场景建议用 remember 缓存,或提取为常量。
十五、本节课知识点总结
15.1 Modifier 本质
15.2 尺寸修饰符
15.3 内外边距
15.4 背景、边框、圆角
15.5 阴影与层级
15.6 点击与交互
15.7 组件样式定制
15.8 布局与样式思维
十六、下节预告
第 4 课:状态与重组——让界面真正活起来。
你将掌握:
- remember 与 mutableStateOf 的深入原理。
- mutableIntStateOf、mutableStateListOf 等状态类型。
- rememberSaveable:配置变更后保存状态。
- State Hoisting:状态提升的完整模式。
- 单向数据流:状态向下,事件向上。
- derivedStateOf:派生状态。
- snapshotFlow:状态与协程的桥接。
- 实战:购物车数量、清单勾选、筛选状态。
附:系列 20 课规划(已更新最后两课)
| 1 | 开课与第一个 Compose | 声明式 UI、环境搭建、@Composable、@Preview | 计数器 |
| 2 | 布局系统 | Column、Row、Box、Spacer、Arrangement、Alignment | 个人名片 |
| 3 | Modifier 与样式 | padding、size、background、border、clip、clickable | 卡片样式 |
| 4 | 状态与重组 | remember、mutableStateOf、State Hoisting | 购物车数量 |
| 5 | 列表与脚手架 | LazyColumn、LazyRow、Scaffold、TopAppBar | 待办列表 |
| 6 | 输入与表单 | TextField、Checkbox、Switch、RadioButton | 登录表单 |
| 7 | 主题与设计系统 | Material 3、ColorScheme、Typography、Shape、暗黑模式 | 一键换肤 |
| 8 | 导航 | Navigation Compose、路由、参数、底部导航 | 三页导航 |
| 9 | 架构与状态管理 | ViewModel、UiState、单向数据流、Repository | 架构版计数器 |
| 10 | 副作用 | LaunchedEffect、DisposableEffect、rememberCoroutineScope | 模拟加载数据 |
| 11 | 动画 | animate*AsState、AnimatedVisibility、Crossfade、Transition | 展开卡片 |
| 12 | 手势与交互 | clickable、pointerInput、draggable、swipe | 滑动删除 |
| 13 | 自定义布局与绘制 | Layout、Canvas、DrawScope、SubcomposeLayout | 环形进度条 |
| 14 | 性能优化 | 重组范围、稳定性、derivedStateOf、key | 优化长列表 |
| 15 | 测试 | Compose UI Test、语义、单元测试 | 测试计数器 |
| 16 | 互操作 | AndroidView、ComposeView、View/Fragment 混合 | 嵌入旧 View |
| 17 | 工程化 | 多模块、Hilt/Koin、资源、预览、调试工具 | 模块拆分 |
| 18 | 响应式与自适应 | WindowSizeClass、平板、折叠屏、多窗口 | 自适应列表 |
| 19 | 经典示例深度解读一 | 选取经典 Compose 示例,深度解读布局、状态、列表、主题、动画、导航等核心实现 | 拆解并重写关键页面 |
| 20 | 经典示例深度解读二 | 选取经典架构示例,深度解读 ViewModel、数据流、网络、分页、测试、性能优化 | 复刻并扩展一个经典示例 |
网硕互联帮助中心




评论前必须登录!
注册