首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

LayoutModifierNode 和 Modifier.layout()

  • 25-04-23 15:08
  • 2196
  • 7741
juejin.cn

前言

我们在写代码时,经常会有很多疑问:为什么这个组件的显示效果和我想的不一样?我该如何精确地控制它的位置和尺寸?Modifier 链的顺序到底有什么影响?

其实这都和Compose 的测量与布局流程有关,本文将带你深入理解 Modifier.layout() 以及它背后的核心实现 LayoutModifierNode,帮助你理清 Compose 布局的原理和实际应用方式。

Modifier.layout()

什么是 Modifier.layout()?

Modifier.layout()是一个修饰符,可以去自定义目标组件的测量和放置过程,从而修改目标组件的尺寸和位置。

kotlin
代码解读
复制代码
// LayoutModifier.kt fun Modifier.layout( measure: MeasureScope.(Measurable, Constraints) -> MeasureResult ) = this then LayoutElement(measure)

这个函数接收一个 lambda 表达式作为参数,该表达式会在布局过程中被调用,负责测量组件并确定其位置。

方法解析

lambda表达式接收两个参数:

kotlin
代码解读
复制代码
Modifier.layout { measurable, constraints -> // 自定义测量和布局 // 返回 MeasureResult }
  • measurable:表示当前被修饰的组件,调用它的measure()方法,就可以确定组件的实际尺寸。

  • constraints:表示父组件对当前组件的约束条件,定义了组件可用的最大和最小尺寸。

Constraints 主要包含 minWidth、maxWidth、minHeight、maxHeight 四个属性,用于限定组件的可用空间。


lambda表达式需要返回一个MeasureResult对象,它包含了组件的最终尺寸、对齐线信息,以及通过placeChildren()方法执行在layout()函数中定义的放置逻辑。

kotlin
代码解读
复制代码
// MeasureResult.kt interface MeasureResult { val width: Int // 组件的最终宽度 val height: Int // 组件的最终高度 val alignmentLines: MapInt> // 对齐线信息 fun placeChildren() // 放置子组件的逻辑 }

我们通常调用MeasureScope 提供的layout()函数来创建MeasureResult实例:

kotlin
代码解读
复制代码
layout(width, height) { // 最终尺寸 // 在这里放置子组件 placeable.placeRelative(x, y) // 内容的位置偏移 }

基本用法

我们现在来看看layout()函数最基本的用法,即不影响组件的测量和布局的写法:

kotlin
代码解读
复制代码
Text(text = "Hello World.", modifier = Modifier.layout { measurable, constraints -> val placeable = measurable.measure(constraints) // 测量Text组件 // 设置最终尺寸 layout(width = placeable.width, height = placeable.height) { // 设置Text组件的偏移量(无偏移) placeable.placeRelative(0, 0) } })

这段代码和直接写Text(text = "Hello World.")的效果相同,我们使用了默认的测量或布局逻辑。

示例:自定义间距修饰符

我们现在来用 layout() 实现一个自定义的间距修饰符,功能类似于官方的 Modifier.padding()。

kotlin
代码解读
复制代码
/** * 自定义间距修饰符 * 在组件四周添加指定的间距 */ fun Modifier.spacing( all: Dp = 0.dp, start: Dp = all, top: Dp = all, end: Dp = all, bottom: Dp = all ) = layout { measurable, constraints -> // 转换 dp 到像素(px) val startPx = start.roundToPx() val topPx = top.roundToPx() val endPx = end.roundToPx() val bottomPx = bottom.roundToPx() // 计算水平和垂直间距总和 val horizontalPadding = startPx + endPx val verticalPadding = topPx + bottomPx // 修改约束条件,减少内容可用空间 val newConstraints = constraints.copy( maxWidth = if (constraints.maxWidth != Constraints.Infinity) { max(constraints.maxWidth - horizontalPadding, 0) } else Constraints.Infinity, maxHeight = if (constraints.maxHeight != Constraints.Infinity) { max(constraints.maxHeight - verticalPadding, 0) } else Constraints.Infinity, // 如果原始约束有最小宽度/高度要求,也需要相应调整 minWidth = max(constraints.minWidth - horizontalPadding, 0), minHeight = max(constraints.minHeight - verticalPadding, 0) ) // 测量内容,使用修改后的约束条件来测量 val placeable = measurable.measure(newConstraints) // 计算最终尺寸 val width = placeable.width + horizontalPadding val height = placeable.height + verticalPadding // 返回测量结果 layout(width, height) { // 放置子组件(有偏移) placeable.placeRelative(startPx, topPx) } }

使用示例:

kotlin
代码解读
复制代码
Column(Modifier.fillMaxWidth()) { Text( "标准 padding", Modifier .background(Color.Yellow) .padding(16.dp) .background(Color.LightGray) ) Spacer(Modifier.height(8.dp)) Text( "自定义 spacing", Modifier .background(Color.Yellow) .spacing(top = 24.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) .background(Color.LightGray) ) }

示意图:

其中黄色部分都是间距,灰色部分才是组件的实际内容区域。

应用标准padding修饰符和自定义spacing修饰符的效果

constraints参数本来是外层组件对当前被修饰的组件的尺寸限制,当我们使用layout()修饰符后,我们插入了一个LayoutModifierNode到布局链中,它可以拦截和修改约束条件。这个节点成为了约束传递的中间者,可以根据需要修改约束条件后再传递给被修饰的组件,就像上面的示例一样。

注意所有参数值都是像素(px),而不是 Dp。如果要使用 Dp,需要转换,调用roundToPx()函数,比如8.dp.roundToPx(), 不过使用这个函数要在Density的上下文中。

使用场景

Modifier.layout()是用来修改组件的尺寸和位置的,它的本质是给组件的位置和尺寸添加装饰效果,不干涉这个组件内部的测量和布局规则。

所以它的使用场景就很明确了,如果你对一个组件的本身很满意,只要对组件的尺寸、位置做一些额外的调整,就可以使用Modifier.layout();如果对组件的内部布局不满足,就要去修改内部源码(如果源码不属于你,可以将代码抄一份过来,再进行修改),不能使用Modifier.layout()了,因为它触及不了那么深的地方。

LayoutModifierNode

LayoutModifierNode 是 Modifier.layout() 背后的核心实现,它是一个接口,它还是Modifier.Element的子类,专门用于修改布局过程的修饰符节点。它通过实现measure方法来拦截和修改测量过程。

kotlin
代码解读
复制代码
interface LayoutModifierNode : DelegatableNode { // 核心方法,负责修改测量过程 fun measure( measureScope: MeasureScope, measurable: Measurable, constraints: Constraints ): MeasureResult }

那LayoutModifierNode是怎么影响测量和布局过程的呢?

我们先来看看元素的测量和布局过程。

元素的测量和布局过程

Composable函数,在实际运行时,会生成LayoutNode对象,它会去做实际的测量、布局、绘制、触摸反馈等工作。

测量和布局工作主要通过内部的 remeasure() 和 replace() 函数完成。

我们来看看测量函数 remeasure() :

kotlin
代码解读
复制代码
// 位于LayoutNode.kt fun remeasure( constraints: Constraints? = layoutDelegate.lastConstraints ): Boolean { //.. measurePassDelegate.remeasure(constraints) }

其中measurePassDelegate是专门用于做测量的工具,点进去这个remeasure()函数:

kotlin
代码解读
复制代码
// MeasurePassDelegate.kt fun remeasure(constraints: Constraints): Boolean { // .. performMeasure(constraints) }

代码很长,但是不必管它,关键代码就只有performMeasure(constraints),它是真正做测量工作的,点进去:

kotlin
代码解读
复制代码
fun performMeasure(constraints: Constraints) { //.. performMeasureBlock //.. }

performMeasureBlocklambda表达式,它是实际完成测量工作的,点进去可以看到:

kotlin
代码解读
复制代码
val performMeasureBlock: () -> Unit = { outerCoordinator.measure(performMeasureConstraints) }

再点进去这个measure()方法,发现竟然来到了Measurable接口:

kotlin
代码解读
复制代码
interface Measurable : IntrinsicMeasurable { fun measure(constraints: Constraints): Placeable }

说明outerCoordinator还没有实现measure()方法,所以要看看创建outerCoordinator时,传入的实际对象类型,那里面才真正实现了measure()方法。


回退到performMeasureBlocklambda表达式中,点击outerCoordinator到它的定义处:

kotlin
代码解读
复制代码
class LayoutNodeLayoutDelegate( private val layoutNode: LayoutNode, ) { val outerCoordinator: NodeCoordinator get() = layoutNode.nodes.outerCoordinator // .. }

点击 layoutNode.nodes:

kotlin
代码解读
复制代码
// LayoutNode.kt internal val nodes = NodeChain(this)

再进入NodeChain:

kotlin
代码解读
复制代码
internal class NodeChain(val layoutNode: LayoutNode) { internal val innerCoordinator = InnerNodeCoordinator(layoutNode) internal var outerCoordinator: NodeCoordinator = innerCoordinator private set }

发现outerCoordinator被赋值为innerCoordinator,而innerCoordinator的类型是InnerNodeCoordinator。

我们进入到InnerNodeCoordinator中查看measure()方法的具体实现:

kotlin
代码解读
复制代码
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) { // before rerunning the user's measure block reset previous measuredByParent for children layoutNode.forEachChild { it.lookaheadPassDelegate!!.measuredByParent = LayoutNode.UsageByParent.NotUsed } val measureResult = with(layoutNode.measurePolicy) { measure( layoutNode.childLookaheadMeasurables, constraints ) } measureResult }

就是在这完成最终、实际的测量的。

方法中调用了measure()方法,返回了一个测量结果给measureResult,用来稍后在布局流程里面用来摆放组件用的。

测量过程:

元素的测量过程

现在我们知道了测量过程中会干什么:会调用NodeCoordinator中的measure()方法。

在布局过程中会获取测量结果,去应用到实际的组件上。

怎么影响测量和布局过程

那LayoutModifierNode到底是怎么影响的?

它的工作原理都包含在了LayoutNode的modifier属性里了。

LayoutNode是运行时实际代表Composable函数的UI节点,我们给每一个Composable函数填写的Modifier参数,经过预处理工作(主要是去掉ComposedModifier),最终会成为LayoutNode的modifier属性,它是一个Modifier链。

我们现在来看看LayoutNode的modifier属性的set()函数:

kotlin
代码解读
复制代码
override var modifier: Modifier = Modifier set(value) { //.. nodes.updateFrom(value) //.. }

nodes.updateFrom(value)会将我们的Modifier链,转换成Modifier.Node双向链表,将Node双向链表交给nodes进行管理。

kotlin
代码解读
复制代码
NodeChain(..) { val innerCoordinator: InnerNodeCoordinator var outerCoordinator: NodeCoordinator val tail: Modifier.Node var head: Modifier.Node }

并且方法中会调用syncCoordinators()进行同步,为每个 Modifier.Node 关联对应的 NodeCoordinator 辅助对象,这些 NodeCoordinator 形成一个链式结构,在测量过程中,约束条件从外层NodeCoordinator传递到内层,然后测量结果再从内层传回外层,最终确定组件的尺寸和位置。

转换过程

LayoutModifierNodeCoordinator 是 LayoutModifierNode 对应的 NodeCoordinator 实现,它的 measure 方法就是 LayoutModifierNode 影响测量过程的关键,每个 LayoutModifierNode 都可以修改约束条件

Modifier 链顺序的实际影响

那我们知道了LayoutModifierNode是怎么影响测量和布局过程:在测量过程中修改约束条件。

那么它Modifier 链顺序的实际影响是什么?

比如

kotlin
代码解读
复制代码
Box(Modifier.size(100.dp).size(200.dp)) // 最终大小 100dp Box(Modifier.size(200.dp).size(100.dp)) // 最终大小 200dp

为什么是这样?

情况一:第一个 size(100.dp)把父约束(比如可能是无穷大)限制成最大100dp,再传递下去。而第二个 size(200.dp)接收到的约束已经是最大100dp了,再怎么设置200dp也没用,因为不能突破前面传下来的限制。 于是它会在“最大100dp”的约束下测量,最终尺寸就是100dp。

Box的最终大小是100.dp

情况二:外层的size(200.dp)将约束设为最大200dp,然后内层的size(100.dp)将约束进一步限制为最大100dp。内容在100dp的约束下测量,得到100dp的尺寸。然后这个测量结果向外传递,外层的size(200.dp)修饰符接收到这个结果,但它会强制将最终尺寸设置为200dp(因为这是它的固定尺寸设置)。

所以Box的最终大小是200dp。

注:本文转载自juejin.cn的雨白的文章"https://juejin.cn/post/7496021698958884902"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top