当前位置: 首页 > news >正文

Compose 修饰符 - 原理

一、概念

不可变性每个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 效果:

  1. 保存 (Save)当前的绘制状态。
  2. 创建一个新的、独立的 Layer (离屏缓冲区)。
  3. 把内容绘制到这个新 Layer 上。
  4. 对整个 Layer 应用alpha效果。
  5. 把修改后的 Layer 绘制回主画布。
  6. 恢复 (Restore)原始的绘制状态。

每一次 save 和 restore 都是有成本的。当把这些绘制类型的修饰符分开写时,就相当于在反复支付这个“图层税”。使用 Modifier.graphicsLayer() 相当于批处理,一次性完成多种图形变换,减少GPU过度绘制避免重复计算。由于只是绘制,当它读取状态时,请求的是 GPU 重绘,完全绕过了 Kotlin 代码的执行逻辑。这几乎是 0 CPU 开销

graphicsLayer()fun Modifier.graphicsLayer(block: GraphicsLayerScope.() -> Unit): Modifier
GraphicsLayerScope

interface GraphicsLayerScope : Density {
var scaleX: Float //X轴缩放
var scaleY: Float //Y轴缩放
var alpha: Float //透明度
var translationX: Float
var translationY: Float
var shadowElevation: Float //阴影高度
var ambientShadowColor: Color
var spotShadowColor: Color
var rotationX: Float //X轴旋转角度
var rotationY: Float //Y轴旋转角度
var rotationZ: Float //Z轴旋转角度
var cameraDistance: Float //查看图层的距离(拉远拉进)
var transformOrigin: TransformOrigin

var clip: Boolean //是否裁切
var shape: Shape //形状
var renderEffect: RenderEffect? //高斯模糊
var blendMode: BlendMode //混合模式
var colorFilter: ColorFilter? //色彩滤镜
var compositingStrategy: CompositingStrategy
val size: Size //尺寸
}

错误示范:三种绘制类型修饰符分别创建了独立的图层,并且 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 {
interface Element : 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(
internal val outer: Modifier, //当前的 Element
internal val inner: Modifier //新增的 Element
) : Modifier

内部维护的数据结构,用于连接调用链中的每个 Element 结点,类似于俄罗斯套娃一样的装饰者模式。

then() 函数

用于连接两个 Element 的方法,底层就是用的 CombinedModifier 结构。

composed() 扩展函数

fun Modifier.composed(
inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
factory: @Composable Modifier.() -> Modifier
): Modifier

内部持有一个工厂 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(
mergeDescendants: Boolean = false,
properties: (SemanticsPropertyReceiver.() -> Unit)
)

允许向当前Compose控件添加键值对形式的额外信息,但是不能覆写。

.clearAndSetSemantics(
properties: (SemanticsPropertyReceiver.() -> Unit)
)

相对用得更多一些,它会把Compsoe控件之前携带的一些额外信息都清除掉。

http://www.jsqmd.com/news/823024/

相关文章:

  • DroidCam OBS插件终极指南:快速实现手机摄像头直播的完整方案
  • StreamCap:如何一站式解决40+直播平台录制难题?
  • Cadence ADE XL/Explorer仿真效率翻倍指南:从多核设置到结果管理全流程
  • LZ4测试覆盖率提升终极指南:模糊测试与边界用例完整解析 [特殊字符]
  • 为ai agent框架配置taotoken作为多模型供应商指南
  • 2026年展柜价格,昂兴文博产品价格合理 - mypinpai
  • 2026年3C充电堆厂家哪家靠谱:合规资质、品控体系与大功率稳定性深度解析 - 科技焦点
  • 2026年5月台州模内贴标定制厂家推荐:模内贴标/热转印/不干胶/塑料皮垫,认准台州市欧玮印务有限公司 - 2026年企业推荐榜
  • 推荐口碑之选:南昆山可篝火溯溪团建别墅选购指南 - 奔跑123
  • 3步解锁PS3手柄新功能:DsHidMini驱动让您的老手柄重获新生
  • 能源展台设计搭建公司怎么选?2026 美国阿纳海姆输配电展优质服务商盘点 - 资讯焦点
  • 正交矩阵:从游戏引擎的旋转矩阵到机器学习PCA,理解这个性质就够了
  • 基于模块化与事件驱动的AI智能体平台构建实战:从原理到部署
  • 2026年四川惠派新材料满意度排名 - mypinpai
  • Adobe Express扩展开发全攻略:从架构设计到部署上线的完整实践
  • 五分钟完成python脚本对接taotoken多模型api的教程
  • 山东靠谱双眼皮医院排行盘点 正规机构实力全解析 - 资讯焦点
  • 破局海外医疗展|2026 杜塞尔多夫医疗展设计搭建公司实力盘点 - 资讯焦点
  • OpenHarmony PWM驱动开发实战:基于RK2206的IoT接口详解与调测
  • Linux内核镜像构建与管理:从源码到部署的工程化实践
  • 基于Vue 3 Composition API的高性能日期选择器架构深度解析与TypeScript类型安全设计
  • 2026 迪拜医疗展设计搭建公司甄选:医疗展台筑造实力之选 - 资讯焦点
  • AGIAgent实践指南:构建可规划、有记忆的AI智能体系统
  • 除了建模,FreeCAD还能怎么用?聊聊0.18.4版本里的Python脚本与二次开发潜力
  • AI视频影视动画培训机构该如何选择 - 速递信息
  • 【Unity进阶实战】将PC端EXE打包与压缩一体化:从项目设置到单文件发布
  • 国内主流防火门生产厂家综合实力排行盘点 - 奔跑123
  • 构建企业级安全运维体系:从SSH堡垒机到自动化管控平台
  • 2026年复合调味粉品牌推荐,如何选择? - 工业推荐榜
  • 智能绝缘电阻测试仪哪家好?2026年国产品牌推荐与选型权威指南 - 品牌推荐大师1