一、概念
| 不可变性 | 每个Modifier操作(如padding/background)都会返回一个新的Modifier,原Modifier不变,避免副作用。 |
| 链式调用 | 通过链式调用连接多个修饰符,按“从左到右”的顺序累加生效,顺序影响最终效果。 |
| 装饰者模式 | 每个修饰符不修改组件本身,而是包装组件,添加额外能力(如clickable为组件添加点击事件)。 |
| 单一职责 | 每个修饰符只做一件事(如padding仅处理边距,background仅处理背景),便于组合和复用。 |
在 NodeKind.kt 文件中根据功能对 Modifier 类型进行了分类。

1.1 什么时候用到
三大使用场景:
1.2 调用顺序(优化)
每一个 Modifier 都是对前一个结果的再包装,调用顺序的不同直接影响最终效果。先通过布局类型明确大小位置 → 再通过绘制类型进行内容装修 → 最后通过交互类型让成本响应用户操作。遵循这个顺序,确保组件行为符合直觉且性能最高。
- 布局类型:size/padding/weight等大小,align/offset等位置。
- 绘制类型:background/border/clip等。
- 交互类型:clickable/draggable/focusable等。
1.2.1 减少布局计算
用 remember() 缓存需要复杂计算的尺寸/位置,避免每次布局阶段重复计算。
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val itemWidth by remember(screenWidth) {
derivedStateOf { screenWidth * 0.8f }
}
Box(modifier = Modifier.size(itemWidth)) {}
1.2.2 使用带 Lambda 参数版本的 Modifier 读取状态
应该尽量避免在 Modifier 修饰符上读取状态,因为它并不是用来显示数据的地方。标准的 Modifier 函数是一定会在组合阶段被执行的,当状态变化时会重新创建 Modifier 实例,UI树会先删除旧的再添加新的实例,而UI树的变化会导致重组,每次重组都可能触发 组合→布局→绘制 三个阶段,若读取的是一个频繁变化的状态非常要命(动画或滚动)。

val color by animateColorBetween(Color.Blue, Color.Red)
Box(
modifier = Modifier.background(color) //Box必须在每一帧上重组,因为每一帧的颜色值都在变化
)
有时不可避免需要一直读取一个不断变化的值从而达到变化的绘制效果。带 Lambda 参数版本的 Modifier 函数不会在组合阶段被执行(根据修改类型延迟到布局阶段或绘制阶段),Modifier实例不会改变,因此UI树不会变化,Compose只会在需要的时候调用 Lambda,也就可以跳过不必要的重组了。
@Composable
fun Demo() {
var offsetX by remember { mutableStateOf(0f) }
val modifier1 = Modifier.offset(x = offsetX.dp) //直接读取
val modifier2 = Modifier.offset{ val newX = offsetX.dp } //在Lambda中读取
}
1.2.3 合并图形操作 graphicsLayer()
绘制类型的修饰符,底层都是使用 Modifier.graphicsLayer() 实现的,都会创建一个图形层,例如应用 alpha 效果:
- 保存 (Save) 当前的绘制状态。
- 创建一个新的、独立的 Layer (离屏缓冲区)。
- 把内容绘制到这个新 Layer 上。
- 对整个 Layer 应用
alpha效果。- 把修改后的 Layer 绘制回主画布。
- 恢复 (Restore) 原始的绘制状态。
每一次 save 和 restore 都是有成本的。当把这些绘制类型的修饰符分开写时,就相当于在反复支付这个“图层税”。使用 Modifier.graphicsLayer() 相当于批处理,一次性完成多种图形变换,减少GPU过度绘制避免重复计算。由于只是绘制,当它读取状态时,请求的是 GPU 重绘,完全绕过了 Kotlin 代码的执行逻辑。这几乎是 0 CPU 开销。
| graphicsLayer() | fun Modifier.graphicsLayer(block: GraphicsLayerScope.() -> Unit): Modifier |
| GraphicsLayerScope | interface GraphicsLayerScope : Density { var clip: Boolean //是否裁切 |
错误示范:三种绘制类型修饰符分别创建了独立的图层,并且 RoundedCornerShape(12.dp) 这个形状的路径计算也可能被重复执行了多次。
modifier = Modifier
//Layer1:透明度
.alpha(0.5F)
//Layer2:裁剪
.clip(RoundedCornerShape(12.dp))
//Layer3:阴影
.shadow(8.dp, RoundedCornerShape(12.dp))
正确使用:
modifier = Modifier
.graphicsLayer {
alpha = 0.5F,
shadowElevation = 8.dp.toPx(),
shape = RoundedCornerShape(12.dp),
clip = true,
renderEffect = BlurEffect(10f, 10f)
}
.background(Colors.red) //背景会被绘制在上面创建的Layer内部并自动被裁剪
1.3 为组合函数添加 Modifier 参数
任何一个组合项都应该有一个 Modifier 参数,以便让调用方进行调整。
- 放在第一个可选参数位置:由于是可选参数,放在所有必传参数后面,这样调用方就可以选择是否传递那些有默认值的可选参数,否则就必须被强制性的先指定 Modifier 才能传必传参数。
- 作用于内部根节点上:调用方一般只需要调整根节点的布局,对于内部子元素的配置可通过传递其它的参数来配置。
- 避免重复使用:将同一个 Modifier 传递给不同的可组合共享,可能引起不必要的重组。
- 同名覆盖问题:通过 then() 可以定义组件内部配置和外部传入配置的顺序。对尺寸的设置参考 Constrains(详见),一般都会用具体值(fillMaxSize、100.dp)因此以第一个为准,约束范围写死了(最大值最小值都是100dp)后续再调用不会生效。offset 多次调用会多次偏移。background 默认情况下只看得到后一个,如果多次调用之间存在 padding、 offset 就不会完全覆盖,可以看到之前的颜色。
@Composable
fun Out() {
//调用时指定对齐方式
In(Modifier.background(Color.Red))
}
@Composable
fun In(
text: String,
modifier: Modifier = Modifier, //放在第一个可选参数位置
enable: Boolean = true
) {
// 传入的 Modifier 作用域根部节点
Box(
// 举例一:这里把外部配置放在了最前面,最终显示 Blue
modifier = modifier
.background(Color.Blue)
// 举例二:这里把外部配置放在了后面,最终显示 Red
modifier = Modifier
.background(Color.Blue)
.then(modifier)
)
}
1.3 底层结构原理
Modifier底层是一个链表结构,每个修饰符节点包含Element(具体操作)和next(下一个修饰符),Compose在测量/布局/绘制组件时,会遍历修饰符链表,依次应用每个修饰符的逻辑。
Modifier#then 创建 Element 加入 Modifier chain 中。Element 是无状态的,重组中会重新生成,Element 会在组合中创建有状态的 Modifier Node。Modifier Node 有状态,重组中仅当状态发生变化时被更新,否则不会重新生成。Modifier Node 是 Compose 1.5 引入的新优化,目的就是通过存储 Modifier 状态参与比较,提升重组性能。
| interface Modifier { companion object : Modifier {...} class CombinedModifier {...} fun then() {...} fun Modifier.composed() {...} | |
| Element 子接口 | 调用 Modifier 不同的配置方法会返回各种 Modifier 的实现类对象(如 .size() 返回 SizeElement、.background() 返回 BackgroundElement),这些实现类又都是 Element 类型。 |
| companion 伴生对象 | 伴生对象实现了 Modifier,因此类名 Modifier 可以用作链式调用的开头。 |
| CombinedModifier | class CombinedModifier( 内部维护的数据结构,用于连接调用链中的每个 Element 结点,类似于俄罗斯套娃一样的装饰者模式。 |
| then() 函数 | 用于连接两个 Element 的方法,底层就是用的 CombinedModifier 结构。 |
| composed() 扩展函数 | fun Modifier.composed( 内部持有一个工厂 Lambda 来生产 Modifier,用于实现内部有状态的 Modifier,如监听手势的修饰符 .pointerInput() 底层就是用到 composed()。 |
1.3.1 链式调用


Modifier.size(100.dp).background(Color.Red).padding(10.dp)
当链式调用 Modifier 的时候,先调用的会包裹后调用的,最里层是 Layout Node。
- 调用 .size() 生成 SizeElement。
- 调用 .background() 生成 CombinedModifier(outer = SizeElement, inner = BackgroundElement)。
- 调用 .padding() 生成 CombinedModifier(outer = CombinedModifier(SizeElement, BackgroundElement), inner = PaddingElement)。
二、自定义 Modifier(优化)
2.1 提取重复使用的效果
将相同的效果(修饰符链)提取到变量中,以便在多个组件中重复使用,这样做有助于提高代码可读性,还有助于提升应用性能。
- 对使用修饰符的可组合项进行重组时,不会重新分配修饰符。
- 修饰符链可能很长并且非常复杂,因此重复使用相同的链实例可以减轻 Compose 运行时在对比时所需的工作量。
- 这种提取方式可提高整个代码库中的代码简洁性、一致性和可维护性。
2.1.1 简单使用
注意有些修饰符限定了作用域,只能在特定场景下使用,如 BoxScope 的 matchParentSize()
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)
@Composable
fun Demo() {
Box(modifier = reusableModifier)
Box(modifier = reusableModifier)
}
2.1.2 观察频繁变化的状态时
在组件中观察频繁变化的状态时(如动画状态、scrollState)可能引发大量重组,在这种情况下,Modifier 会在每次重组时创建实例,这可能发生在动画的每一帧。可以提取 Modifier 到可组合项外面来重用。
val reusableModifier = Modifier
.padding(12.dp)
.background(Color.Gray)
@Composable
fun Demo() {
val animatedState = animateFloatAsState(/*...*/)
LoadingWheel(
// 无需分配内存,因为只是重复使用同一个实例
modifier = reusableModifier,
animatedState = animatedState
)
}
2.1.3 延迟布局条目复用
延迟布局会大量创建条目,复用 Modifier 可以优化内存。
val reusableItemModifier = Modifier
.padding(bottom = 12.dp)
.size(216.dp)
.clip(CircleShape)
@Composable
private fun Demo(authors: List<Author>) {
LazyColumn {
items(authors) {
// 无需分配内存,因为我们只是重复使用同一个实例。
AsyncImage(modifier = reusableItemModifier)
}
}
}
2.1.4 进一步链接提取的修饰符
通过调用 then() 进一步链接或附加提取的修饰符链。
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)
// 追加到自己的修饰符后面
reusableModifier.clickable { /*...*/ }
// 追加到别人的后面
otherModifier.then(reusableModifier)
2.2 自定义带参数的修饰符
fun Modifier.cardStyle(
backgroundColor: Color = Color.White,
cornerRadius: Dp = 8.dp,
elevation: Dp = 4.dp
): Modifier {
return this
.background(backgroundColor, shape = RoundedCornerShape(cornerRadius))
.shadow(elevation, shape = RoundedCornerShape(cornerRadius))
.padding(16.dp)
}
2.3 自定义 Element
//自定义测量和摆放
fun Modifier.XXX(): Modifier = then(
layout { measurable, constraints ->
//TODO...
}
}
//去掉涟漪效果
fun Modifier.clickableNoRipple(onClick: () -> Unit): Modifier = composed {
clickable(
onClick = onClick,
interactionSource = remember { MutableInteractionSource() },
indication = null
)
}
三、添加额外信息

在Compose的内部,是用树型结构来存储一次重组过程中每个Composable函数节点的。一颗就是我们现在看到的重组树,另外一颗则是我们看不到的语义树。
语义树完全不参与绘制和渲染工作,因此是完全不可见的,它只为 Accessibility 和 Test 服务。Accessibility需要根据语义树的节点内容进行发音,Test则需要根据语义树找到想要测试的节点来执行测试逻辑。
绝大部分情况下不需要专门为语义树去做什么事情,标准的组合项已经在内部处理好了这些工作(Button嵌套一个Text,它俩是独立控件,Talkback会单独发声,但只要控件可点击,就会自动将所有子节点合并)。若使用了一些底层API自行绘制界面(日历选中8号,只会发音选中日历),这些工作就得自己来做了。
| .semantics( 允许向当前Compose控件添加键值对形式的额外信息,但是不能覆写。 |
| .clearAndSetSemantics( 相对用得更多一些,它会把Compsoe控件之前携带的一些额外信息都清除掉。 |
本文围绕Android中Compose的Modifier展开。介绍了Modifier的使用场景、添加参数的规则及底层结构原理。还阐述了通过它修改外观(尺寸、样式等)、添加额外信息、交互功能(点击、滚动等)以及处理用户输入的方法,帮助开发者更好地运用Modifier。
591

被折叠的 条评论
为什么被折叠?



