深度解析 Compose 的 Modifier 原理 -- Modifier.layout()、LayoutModifier

在这里插入图片描述


" Jetpack Compose - - Modifier 原理系列文章 "


    📑 《 深入解析 Compose 的 Modifier 原理 - - Modifier、CombinedModifier 》

    📑 《 深度解析 Compose 的 Modifier 原理 - - Modifier.composed()、ComposedModifier 》

    📑 《 深入解析 Compose 的 Modifier 原理 - - Modifier.layout()、LayoutModifier 》

    📑 《 深度解析 Compose 的 Modifier 原理 - - DrawModifier 》

    📑 《 深度解析 Compose 的 Modifier 原理 - - PointerInputModifier 》

    📑 《 深度解析 Compose 的 Modifier 原理 - - ParentDataModifier 》


在正式开始分析 LayoutModifier 相关原理之前,建议你先看看 Compose 是如何将数据转换成 UI 的?这篇文章,当你了解了 Compose 的“组合”、“布局”、“绘制”的思维模型后,有助于你更透彻的了解 Modifier 的底层设计原理。如果此时你对 Modifier 还不了解,可以先阅读前面两篇关于 Modifier 的文章,这些都是必备基础。

一、场景引入

1.1 一个 Text()

先来看一个最简单的代码示例:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose")
                }
            }
        }
    }
}

这段代码很简单,效果图如下:

在这里插入图片描述

接下来基于这个 Demo,我们会慢慢引入本篇文章的主角。

1.2 Modifier.layout()

在 Compose 中 Modifier.layout() 是一种布局修饰符,它会包裹一个布局节点:Layout(什么是 LayoutNode,下面会讲),通常用作对目标组件进行测量和位置摆放的。

说白了就是你可以用 Modifier.layout() 来自定义目标组件的测量过程以及决定目标组件怎么摆放。

现在我们看看代码中怎么用,通常会像下面这样写:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->  }
                    )
                }
            }
        }
    }
}

就这么简单,Text 就是目标组件,我们给它加了一个 Modifier.layout(),没有添加任何其他代码逻辑,{ measurable, constraints -> } 是自动生成的,此时你会发现在 Android Studio IDLE 中,这样写是会标红的:

在这里插入图片描述

正常来说,我这里什么也不填不就相当于对 Text() 不做任何修饰。但很明显这样是不行的,接下来我们一起尝试解决这个报错。

首先我们发现当使用 layout() 修饰符时,传入的回调 lambda 包含了两个参数:

  1. measurable:用于子元素的测量和位置放置;
  2. constraints:用于约束子元素 width 和 height 的最大值和最小值。

我们定位到 Modifier.layout() 的源码:

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutModifierElement(measure)

measurable 对应的是 Measurable:

interface Measurable : IntrinsicMeasurable {
    // 返回一个 Placeable,它里面包含目标组件的宽、高等信息
    fun measure(constraints: Constraints): Placeable
}

Measurable 是一个接口,内部仅有一个 measure() 方法。

所以现在可以开始修改刚才的报错了:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            // 用一个变量保存返回的 Placeable 对象
                            val placeable = measurable.measure(constraints)
                        }
                    )
                }
            }
        }
    }
}

现在代码仍然是标红报错的,原因在于:我们只处理了 measurable,它返回的是 Placeable,而 Modifier.layout() 需要返回的类型是 MeasureResult

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutModifierElement(measure)

所以 MeasureResult 是什么?

interface MeasureResult {
    val width: Int
    val height: Int
    val alignmentLines: Map<AlignmentLine, Int>
    fun placeChildren()
}

MeasureResult 也是一个接口,它里面也有 widthheight,继续修复刚才的报错:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Text("ComposeTest",
                    Modifier.layout { measurable, constraints ->
                        val placeable = measurable.measure(constraints)
                        object : MeasureResult {
                            override val alignmentLines: Map<AlignmentLine, Int>
                                get() = TODO("Not yet implemented")
                            override val height: Int
                                get() = TODO("Not yet implemented")
                            override val width: Int
                                get() = TODO("Not yet implemented")

                            override fun placeChildren() {
                                TODO("Not yet implemented")
                            }
                        }
                    }
                )
            }
        }
    }
}

既然 Modifier.layout() 需要一个 MeasureResult 返回对象,那我们就在内部给它创建一个 MeasureResult 对象,此时 IDLE 就不会再报错了。

当然我们还需要做一个工作,那就是把 placeable 的宽高传进 MeasureResult 内部,所以最终的代码修改如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            object : MeasureResult {
                                // 测量基准线,暂时不用关心
                                override val alignmentLines: Map<AlignmentLine, Int>
                                    get() = TODO("Not yet implemented")
                                // 高:placeable.height
                                override val height: Int
                                    get() = placeable.height
                                // 宽:placeable.width
                                override val width: Int
                                    get() = placeable.width

                                // 摆放内部组件,暂时不用关心
                                override fun placeChildren() {
                                    TODO("Not yet implemented")
                                }
                            }
                        }
                    )
                }
            }
        }
    }
}

至此报错就修复了,上面的代码演示了对 Text() 添加 Modifier.layout() 进行修饰(当然上面的做法等同于啥也没做)。

但这段代码有个缺陷:如果每次通过 Modifier.layout() 对组件修饰,都得像上面这样写一堆代码,那还不得疯?

1.3 layout() 函数

其实在实际开发中我们并不会这么写,而是使用 Compose 提供给我们的 layout() 函数

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            /*object : MeasureResult {
                                override val alignmentLines: Map<AlignmentLine, Int>
                                    get() = TODO("Not yet implemented")
                                override val height: Int
                                    get() = TODO("Not yet implemented")
                                override val width: Int
                                    get() = TODO("Not yet implemented")

                                override fun placeChildren() {
                                    TODO("Not yet implemented")
                                }

                            }*/
                            layout() {
                                
                            }
                        }
                    )
                }
            }
        }
    }
}

我们来看下 layout() 源码:

    fun layout(
        width: Int,
        height: Int,
        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
        placementBlock: Placeable.PlacementScope.() -> Unit
    ) = object : MeasureResult {  // 看这里,这是个啥?熟悉吗?
        override val width = width
        override val height = height
        override val alignmentLines = alignmentLines
        override fun placeChildren() {
            Placeable.PlacementScope.executeWithRtlMirroringValues(
                width,
                layoutDirection,
                this@MeasureScope as? LookaheadCapablePlaceable,
                placementBlock
            )
        }
    }

很明显 layout() 函数帮我们创建好了 MeasureResult 对象,同时它还帮我们干了另外两件没做的事:

  1. 给 alignmentLines 设定了默认值;
  2. 实现了 placeChildren()。

所以,我们现在只需要补全 layout() 函数剩余的两个参数:widthheight

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            layout(placeable.width, placeable.height) {

                            }
                        }
                    )
                }
            }
        }
    }
}

这样写代码是不是瞬间感觉清爽了很多?但工作到这边还没有结束,layout() 函数还有第四个参数,是一个 Lambda 表达式,主要工作是处理被修饰组件的摆放规则,比如偏移量。

fun layout(
    width: Int,
    height: Int,
    alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
    placementBlock: Placeable.PlacementScope.() -> Unit    // Lambda 表达式
) = object : MeasureResult {
... ...
}

我们继续完善:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            layout(placeable.width, placeable.height) {
                                // 不做任何偏移
                                placeable.placeRelative(0, 0)
                            }
                        }
                    )
                }
            }
        }
    }
}

现在所有工作(测量 + 摆放)都已完成,运行看下效果:

在这里插入图片描述

可以看出来,没有任何变化,因为我们虽然用 Modifier.layout() 对 Text 做修饰,但并没有对它做任何尺寸修改和位置偏移。

那如果我现在想修改 Text() 的尺寸,该怎么做?

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            val size = min(placeable.width, placeable.height)
                            layout(size, size) {
                                placeable.placeRelative(0, 0)
                            }
                        })
                }
            }
        }
    }
}

我们定义了一个 size 变量,通过 min() 函数获取宽高最小值,然后重新传入 layout() 里面,这样就会获得一个正方形的效果。

在这里插入图片描述

尺寸修改确实生效了,接下来再增加一个偏移:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            val size = min(placeable.width, placeable.height)
                            layout(size, size) {
                                placeable.placeRelative(10, 0)
                            }
                        })
                }
            }
        }
    }
}

看下效果:

在这里插入图片描述

另外有个细节需要说明下,除了使用 placeRelative 对组件偏移外,也可以使用 place 进行偏移操作,两者的区别就是 placeRelative 会自适应 RTL 布局。

讲到这里,Modifier.layout() 修饰符和 layout() 函数的用法你应该都清楚了,但还没结束,前面我们一直忽略了一个参数:constraints,它是什么?

  1. measurable:用于子元素的测量和位置放置的;
  2. constraints:用于约束子元素 width 和 height 的最大值和最小值。

前面的例子并没有对 constraints 做任何修改,在实际开发过程中,我们往往需要通过 constraints 对组件进行限制。

比如我想对 Text() 组件进行一个限制,类似 padding 的效果,给它加一个 10dp 的最大宽高的限制(最大宽高缩减 10dp)。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        Modifier.layout { measurable, constraints ->
                            val paddingPx = 10.dp.roundToPx()
                            val placeable = measurable.measure(constraints.copy(
                                maxWidth = constraints.maxWidth - paddingPx * 2,
                                maxHeight = constraints.maxHeight - paddingPx * 2
                            ))
                            layout(placeable.width + paddingPx * 2, placeable.height + paddingPx * 2) {
                                placeable.placeRelative(paddingPx, paddingPx)
                            }
                        })
                }
            }
        }
    }
}

看下效果:

在这里插入图片描述

很明显,我们实现的效果跟 Modifier.padding(10.dp) 的效果是一样的,如果你去看看 Modifier.padding 的源码,就会发现它的内部原理跟我们例子是一样的。

@Stable
fun Modifier.padding(
    horizontal: Dp = 0.dp,
    vertical: Dp = 0.dp
) = this.then(
    PaddingModifier(
        ... ...
    )
)
private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp,
    val rtlAware: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    ... ...

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }
    ... ...
}

二、LayoutNode 浅析


前面所有的示例代码,不论是使用 Modifier.layout() 修饰符还是使用 Compose 提供给我们的现成的修饰符,比如:Modifier.padding() / Modifier.size(),它们都会对被修饰组件产生精细影响(组件大小、位置偏移)。

但到目前为止,我们仅仅是从 UI 效果上看到 Modifier.layout() 会影响被修饰组件,但源码底层是如何产生影响的呢?这才是我们这篇文章的核心!

所以,最硬核的原理部分来了!

我们就拿常用的 Modifier.padding() 分析:

@Stable
fun Modifier.padding(
    horizontal: Dp = 0.dp,
    vertical: Dp = 0.dp
) = this.then(
    PaddingModifier(
        ... ...
    )
)

private class PaddingModifier(
    ... ...
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {


interface LayoutModifier : Modifier.Element {

Modifier.padding() 内部会调用一个 PaddingModifier 对象,而 PaddingModifier 实现了 LayoutModifier 接口,这个 LayoutModifier 会被 Compose 用于修改测量和布局过程,从而最终影响到界面元素的位置和尺寸。

所以我们的重点就是要研究 LayoutModifier 是如何影响组件的


但是!在分析 LayoutModifier 原理之前,有一个核心知识点是必须要提前了解的。

2.1 一个 Text()

这段代码我们再熟悉不过了:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose")
                }
            }
        }
    }
}

Box 、Text 这些函数在实际运行的时候,其实并不是这些函数直接存在于内存里面,而是 Compose 利用这些函数创造出的一些对象存在于内存里面,这个对象就是:LayoutNode,它才是最底层的那个节点,进行实际的测量、布局、绘制、触摸反馈等工作,你可以查看 Compose 是如何将数据转换成 UI 的?这篇文章,了解转换的思维模型!

我们既然想知道 LayoutModifier 是如何精细影响 Text() 组件,那就得先研究明白 Text() 自己的测量、布局、绘制的原理,因为 LayoutModifier 是包着这个 Text() 的。

在 LayoutNode 中,测量和布局是由 remeasure() 函数和 replace() 两个函数处理。

// LayoutNode.kt
internal class LayoutNode(..) {
    internal fun replace()      // 布局
    internal fun remeasure()    // 测量
}

2.2 LayoutNode.remeasure()

我们先来分析 remeasure() 函数:

// LayoutNode.kt
internal class LayoutNode(..) {

    internal fun remeasure(
        constraints: Constraints? = layoutDelegate.lastConstraints
    ): Boolean {
        return if (constraints != null) {
            if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
                clearSubtreeIntrinsicsUsage()
            }
            // 测量工作交给 LayoutNodeLayoutDelegate 的内部类 MeasurePassDelegate 处理
            measurePassDelegate.remeasure(constraints)
        } else {
            false
        }
    }

}

2.3 MeasurePassDelegate.measurePassDelegate()

LayoutNode 内部要处理的事情非常多,它把测量的工作又交给了 MeasurePassDelegate 来处理。

// LayoutNodeLayoutDelegate.kt
internal class LayoutNodeLayoutDelegate(
    private val layoutNode: LayoutNode
) {

	inner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner {
	
	    fun remeasure(constraints: Constraints): Boolean {
	        ...
	        if (layoutNode.measurePending || measurementConstraints != constraints) {
	            ...
	            performMeasure(constraints)    // 关键代码
	            ...
	        }
	        return false
	    }
	
	}

}

这段代码很长,但我们只需要关注一行关键代码:performMeasure(constraints)

2.4 performMeasure()

// LayoutNodeLayoutDelegate.kt
internal class LayoutNodeLayoutDelegate(
    private val layoutNode: LayoutNode
) {

    private fun performMeasure(constraints: Constraints) {
        ...
        layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
            layoutNode,
            affectsLookahead = false
        ) {
            outerCoordinator.measure(constraints)    // 关键代码
        }
        ...
    }

}

我们仍然只需要关注:outerCoordinator.measure(constraints),它是做实际测量工作的。

2.5 Measurable.measure()

// Measurable.kt
interface Measurable : IntrinsicMeasurable {
    /**
     * Measures the layout with [constraints], returning a [Placeable] layout that has its new
     * size. A [Measurable] can only be measured once inside a layout pass.
     */
    fun measure(constraints: Constraints): Placeable
}

???怎么是个接口啊,没有任何处理逻辑啊。

那肯定有其他地方实现了这个方法,我们可以搜一下哪些地方实现了。

在这里插入图片描述

有这么多地方实现了,但哪一个才是我们需要的?别慌,我带你找一下。

我们往回退,刚才哪里调用 measure 的?

// LayoutNodeLayoutDelegate.kt
internal class LayoutNodeLayoutDelegate(
    private val layoutNode: LayoutNode
) {
    /**
     * 2. outerCoordinator 由传进来的 layoutNode 参数决定,那么我们得找找 layoutNode 是哪里传进来的
     *    这里记住:
     *    a. 接下来我们先找到哪里传入了 layoutNode
     *    b. 找到后我们再看 layoutNode.nodes.outerCoordinator 是什么?
     */
    val outerCoordinator: NodeCoordinator
        get() = layoutNode.nodes.outerCoordinator
    
    private fun performMeasure(constraints: Constraints) {
        ...
        layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
            layoutNode,
            affectsLookahead = false
        ) {
            // 1. 这里调用了 measure(),那么 outerCoordinator 是什么?
            outerCoordinator.measure(constraints)
        }
        ...
    }

}

继续回退到上一层:

// LayoutNodeLayoutDelegate.kt
internal class LayoutNodeLayoutDelegate(
    private val layoutNode: LayoutNode
) {

	inner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner {
	
	    // 2. 我们继续回退
	    fun remeasure(constraints: Constraints): Boolean {
	        ...
	        if (layoutNode.measurePending || measurementConstraints != constraints) {
	            ...
	            // 1. 没有地方传入 layoutNode 啊
	            performMeasure(constraints)
	            ...
	        }
	        return false
	    }
	
	}

}

继续回退到上一层:

// LayoutNode.kt
internal class LayoutNode(..) {

    // 2. 通过 layoutDelegate 获取 measurePassDelegate,那 layoutDelegate 是什么?
    private val measurePassDelegate
        get() = layoutDelegate.measurePassDelegate
    ...
    
    // 4. 来来来,nodes 在这里,NodeChain 又是什么?
    internal val nodes = NodeChain(this)

    /**
     * 3. LayoutNodeLayoutDelegate(this) 看到这行代码你有没有想起来什么?
     *    这个 this 不就是我们要找的那个 layoutNode 参数嘛!
     *    现在我们再看下刚刚用到这个参数的地方:
     *    // 还记得吧?现在 layoutNode 找到了,接下来看看 nodes 是什么?
     *    val outerCoordinator: NodeCoordinator
     *        get() = layoutNode.nodes.outerCoordinator
     */
    internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
    internal val outerCoordinator: NodeCoordinator
        get() = nodes.outerCoordinator

    internal fun remeasure(
        constraints: Constraints? = layoutDelegate.lastConstraints
    ): Boolean {
        return if (constraints != null) {
            ...
            // 1. measurePassDelegate 是什么?
            measurePassDelegate.remeasure(constraints)
        } else {
            false
        }
    }

}

2.6 NodeChain

我们再来看看 NodeChain 是什么?

// NodeChain.kt
internal class NodeChain(val layoutNode: LayoutNode) {
    // 2. innerCoordinator 又是 InnerNodeCoordinator!
    internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
    // 1. layoutNode.nodes.outerCoordinator 是 innerCoordinator
    internal var outerCoordinator: NodeCoordinator = innerCoordinator
        private set
    ... ...
}

代码跟踪到这里就真相显现了,到底是哪个实现类处理了 measure() 方法,你现在清楚了吧?再来看下刚刚的截图:

在这里插入图片描述

2.7 InnerNodeCoordinator.measure()

所以接下来我们就看看 InnerNodeCoordinator 是如何负责具体测量的:

// InnerNodeCoordinator.kt
internal class InnerNodeCoordinator(
    layoutNode: LayoutNode
) : NodeCoordinator(layoutNode) {

    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        layoutNode.forEachChild {
            it.measuredByParent = LayoutNode.UsageByParent.NotUsed
        }

        // 2. 返回一个 MeasureResult 对象给 replace() 去布局
        measureResult = with(layoutNode.measurePolicy) {
            // 1. 最核心处:这边就是最底层开始测量的工作了
            measure(layoutNode.childMeasurables, constraints)
        }
        onMeasured()
        return this
    }

}

分析到这里,关于组件的测量和布局流程就跑通了: 我们在代码中所写的 Box、Text 等组件内部会有自己设定的测量数据,他们在代码实际运行过程中会被 Comopse 转换成 LayoutNode 节点(包含所有组件自身的测量数据),然后一层层往下传,最终传到 InnerNodeCoordinator,由它进行最底层的测量工作,测量完成后会返回一个 MeasureResult 对象再交给 replace() 函数完成布局工作。

所以,Do you understand?

在这里插入图片描述


三、LayoutModifer 的工作原理


前面我们已经了解了组件自身的测量和布局原理,现在就可以开始分析 LayoutModifer 是如何影响组件的测量和布局了。

就像我们前面说的那样,所有组件最终都会被转换为一个 LayoutNode,这个 LayoutNode 包含了所有的测量数据,那同样它也会包含你对组件设定的 Modifier,所以最终经过一些列转换,也会传到 LayoutNode 里面,那 LayoutNode 里面必然会存在一个 modifer 属性来处理你所设定的 Modifer.xx,我们来看源码:

3.1 LayoutNode -> modifier

// LayoutNode.kt
internal class LayoutNode(..) {

    // 可以理解该 modifier 是 Composable 所有 modifier 的组合
    override var modifier: Modifier = Modifier
        // 如果有新值变化
        set(value) {
            ...
            nodes.updateFrom(value)    // 关键代码
            ...
        }
}

3.2 updateFrom()

来看看 updateFrom 的具体工作:

// NodeChain.kt
internal class NodeChain(val layoutNode: LayoutNode) {
    // 负责最内层测量的 NodeCoordinator
    internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
    // 负责外层测量的 NodeCoordinator,初始值是 InnerNodeCoordinator
    internal var outerCoordinator: NodeCoordinator = innerCoordinator
        private set
    // 双向链表的尾节点
    internal val tail: Modifier.Node = innerCoordinator.tail
    // 双向链表的头节点
    internal var head: Modifier.Node = tail
        private set
    ...
    
	internal fun updateFrom(m: Modifier) {
	    ...

        val before = current ?: MutableVector(capacity = 0)
        // fillVector 会将 Modifier 展开铺平到一个数组,后面的代码就可以用这个数组遍历
        val after = m.fillVector(buffer ?: mutableVectorOf())
        if (after.size == before.size) {
            ...
        } else if (before.size == 0) {
            // 第一次组装双向链表
			// 遍历上面铺平的数组,然后将 Modifier 装进 Node 组装成双向链表
            attachNeeded = true
            coordinatorSyncNeeded = true
            var i = after.size - 1
            var aggregateChildKindSet = 0
            var node = tail
            while (i >= 0) {
                val next = after[i]
                val child = node
                // 组装双向链表的具体逻辑
                node = createAndInsertNodeAsParent(next, child)
                ...
                i--
            }
        }
        ...

        // 将头节点和尾节点保存到 head 和 tail
        trimChain()

        // 将 Node 和所属的 NodeCoordinator 挂接关联
        if (coordinatorSyncNeeded) {
            syncCoordinators()
        }
        ...
	}
}

3.3 syncCoordinators()

// NodeChain.kt
internal class NodeChain(val layoutNode: LayoutNode) {

    private fun syncCoordinators() {
        var coordinator: NodeCoordinator = innerCoordinator
        var node: Modifier.Node? = tail.parent
        while (node != null) {
        	// 遇到 LayoutModifier 的处理
			// 创建一个 LayoutModifierNodeCoordinator,也是用来做测量的
			// 让其他 Modifier 会受到某一个 LayoutModifier 的限制影响
            if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
                val next = if (node.coordinator != null) {
                    val c = node.coordinator as LayoutModifierNodeCoordinator
                    val prevNode = c.layoutModifierNode
                    c.layoutModifierNode = node
                    if (prevNode !== node) c.onLayoutModifierNodeChanged()
                    c
                } else {
                    val c = LayoutModifierNodeCoordinator(layoutNode, node)
                    node.updateCoordinator(c)
                    c
                }
                coordinator.wrappedBy = next
                next.wrapped = coordinator
                coordinator = next
            } else {
                // 将 node 和 NodeCoordinator 挂接关联,其实就是记录在变量
                node.updateCoordinator(coordinator)
            }
            node = node.parent
        }
        coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
        outerCoordinator = coordinator    // 调整外层 NodeCoordinator
    }

}

简单总结下 updateFrom() 的处理步骤:

  1. 在 Composable 编写的 Modifier 是层层嵌套的,首先需要将 Modifier 集合铺平到一个数组中(与程序编写 Modifier 的顺序相反展开存到数组),方便后续使用这个数组遍历
  2. 如果 NodeChain 还没有组装过双向链表,遍历步骤一铺平的 Modifier 数组组装成双向链表;否则就对双向链表增量更新
  3. 记录双向链表的头节点和尾节点
  4. 将 Modifier 和所属的 NodeCoordinator 挂接关联。

我们可以梳理一下:

1. 不设置 Modifier

   // 比如:Text()
   outerCoordinator = InnerNodeCoordinator

2. 一个 LayoutModifer

   // 比如:Text(Modifier.padding(10.dp))
   outerCoordinator = 
      LayoutModifierNodeCoordinator[
         LayoutModifier                     // PaddingModifier
         +
         InnerNodeCoordinator
      ]

3. 两个 LayoutModifier

   // 比如:Text(Modifier.padding(10.dp).size(30.dp))
   outerCoordinator = 
      LayoutModifierNodeCoordinator[
         LayoutModifier                     // PaddingModifier
         + 
         LayoutModifierNodeCoordinator[
            LayoutModifier                  // SizeModifier
            +
            InnerNodeCoordinator
         ]
      ]

逻辑非常清晰,一层套一层。

我们之前说过,outerCoordinator 对象是做实际测量工作的,如果有 LayoutModifer,那么就会交由 LayoutModifierNodeCoordinator 做综合测量了。

3.4 LayoutModifierNodeCoordinator.measure()

我们看下 LayoutModifierNodeCoordinator 是怎么做测量的:

// LayoutModifierNodeCoordinator.kt
internal class LayoutModifierNodeCoordinator(
    layoutNode: LayoutNode,
    measureNode: LayoutModifierNode,
) : NodeCoordinator(layoutNode) {

    override fun measure(constraints: Constraints): Placeable {
        performingMeasure(constraints) {
            // 1. 核心代码,with 包含了 LayoutModifierNode,提供了一个 LayoutModifierNode 的上下文
            with(layoutModifierNode) {
                // 2. 那么这个 measure 会跳转到哪里?
                measureResult = measure(wrappedNonNull, constraints)
                this@LayoutModifierNodeCoordinator
            }
        }
        onMeasured()
        return this
    }
    
}

3.5 LayoutModifierNode.measure()

measure() 的工作会跳转到哪里,是由 with() 决定的,源码跳转到了 LayoutModifierNode 里面。

// LayoutModifierNode.kt
interface LayoutModifierNode : Remeasurement, DelegatableNode {

    fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult

    ... ...
}

LayoutModifierNode 只是一个接口,所以具体的测量实现在哪?

跟着我的节奏考虑:

 --> 为什么跳转到了 LayoutModifierNode?-- 因为 with() 里面是一个 LayoutModifierNode 的上下文

        --> LayoutModifierNode 是哪来的?-- 你传进来的

               --> 你从哪传进来的?

还记得一开始修改完报错的全套代码吗:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.background(Color.Green)) {
                    Text("Hi Compose",
                        // here,熟悉吗?我们自己写的代码
                        Modifier.layout { measurable, constraints ->
                            val paddingPx = 10.dp.roundToPx()
                            val placeable = measurable.measure(constraints.copy(
                                maxWidth = constraints.maxWidth - paddingPx * 2,
                                maxHeight = constraints.maxHeight - paddingPx * 2
                            ))
                            layout(placeable.width + paddingPx * 2, placeable.height + paddingPx * 2) {
                                placeable.placeRelative(paddingPx, paddingPx)
                            }
                        })
                }

                Modifier.padding()
            }
        }
    }
}

点进 Modifier.layout {} 看源码:

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(
    LayoutModifierImpl(
        measureBlock = measure,
        inspectorInfo = debugInspectorInfo {
            name = "layout"
            properties["measure"] = measure
        }
    )
)

再点击 LayoutModifierImpl 进去看看:

private class LayoutModifierImpl(
    val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,
    inspectorInfo: InspectorInfo.() -> Unit,
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    // 看!!!是不是重写了 MeasureScope.measure 方法 ? 所以具体测量工作就在这里了,我们找到了!
    // 同理,像 Modifier.padding(),你也可以点进去看源码,同样重写了 MeasureScope.measure 方法。
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ) = measureBlock(measurable, constraints)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        val otherModifier = other as? LayoutModifierImpl ?: return false
        return measureBlock == otherModifier.measureBlock
    }

    override fun hashCode(): Int {
        return measureBlock.hashCode()
    }

    override fun toString(): String {
        return "LayoutModifierImpl(measureBlock=$measureBlock)"
    }
}

这里 measure 直接调用了 measureBlock(),它是参数传进来的,往回退:

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(
    LayoutModifierImpl(
        measureBlock = measure,
        inspectorInfo = debugInspectorInfo {
            name = "layout"
            properties["measure"] = measure
        }
    )
)

measureBlock 又是 measure,而 measure 是啥?就是你主代码里面写在 Modifier.layout {} 里面的测量和布局逻辑。


到这里所有原理都已经讲清楚了,看不懂的话,你就再看一遍!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/331260.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis安装-Linux为例

可以下载一个Shell或者MobaXterm工具&#xff0c;便于操作 在redis官网下载压缩包 开始安装 安装依赖 yum install -y gcc tcl切换目录 切换目录后直接把redis安装包拖到/user/local/src/下 cd /user/local/src/解压然后安装 #解压 tar -zxvf redis-7.2.4.tar.gz #安装 …

C语言——小细节和小知识12

一、倒置句子 将句子中的单词位置倒置&#xff0c;标点不用倒置&#xff0c;例如i love you.倒置结果是&#xff1a;you. love i。 1、两步翻转法 采用两步翻转法来实现单词位置的倒置。首先&#xff0c;它整体翻转整个字符串&#xff0c;然后再逐个翻转每个单词内的字符。 …

环形链表问题

环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&a…

如何录制屏幕视频?让视频制作更简单!

随着数字化时代的来临&#xff0c;录制屏幕视频成为一种常见的传播和教学方式。无论是制作演示文稿、教学视频&#xff0c;还是记录游戏操作&#xff0c;屏幕录制为用户提供了强大而灵活的工具。可是您知道如何录制屏幕视频吗&#xff1f;本文将深入介绍两种常见的屏幕录制方法…

vue el-select自定义搜索选择案例

开发中常见的有选择框并且输入关键词可以快速检索功能&#xff0c;刚好这次项目需求&#xff0c;就开始吧 需求&#xff1a;1、生成1000到100000的数可选择&#xff0c;递增1000 2、这些数必须三位数用逗号隔开&#xff0c;比如1,000.00这样的形式显示 3、输入关键词比如10&am…

zabbix监控平台(agent端)

引言&#xff1a;明人不说暗话&#xff0c;上一篇文章我们讲了zabbix的serrver端部署和配置&#xff0c;今天详细讲解一下agent端服务器&#xff08;客户端&#xff09;的配置和关联 1.进入官网 Zabbix&#xff1a;企业级开源监控解决方案 2.进入下载页面选择需要下载的版本信…

【JVM】JVM概述

JVM概述 基本介绍 JVM&#xff1a;全称 Java Virtual Machine&#xff0c;即 Java 虚拟机&#xff0c;一种规范&#xff0c;本身是一个虚拟计算机&#xff0c;直接和操作系统进行交互&#xff0c;与硬件不直接交互&#xff0c;而操作系统可以帮我们完成和硬件进行交互的工作特…

【网络安全】2024年一个漏洞4w+,网安副业挖SRC漏洞,躺着把钱挣了!

一个漏洞奖励2w&#xff0c;这是真实的嘛&#xff01; 作为资深白帽&#xff0c;入行网安这些年也一直在接私活&#xff0c;副业赚的钱几乎是我工资的三倍&#xff01;看到最近副业挖漏洞的内容非常火爆&#xff0c;我便决定将自己的经验分享出来&#xff0c;带我的粉丝们一起…

Vue3在点击菜单切换路由时,将ElementPlus UI库中el-main组件的内容滚动恢复到顶部

功能&#xff1a;Vue3在点击菜单切换路由时&#xff0c;将页面el-main的内容滚动到顶部&#xff0c;布局如下&#xff0c;使用UI组件库为ElementPlus 在网上搜很多都是在route.js中的router.beforeEach中使用window.scrollTop(0,0) 或 window.scrollTo(0,0) 滚动&#xff0c;但…

深入Docker5:安装nginx部署完整项目

目录 准备 为什么要使用nginx mysql容器构建 1.删除容器 2.创建文件夹 3.上传配置文件 4.命令构建mysql容器 5.进入mysql容器&#xff0c;授予root所有权限 6.在mysql中用命令运行sql文件 7.创建指定数据库shop 8.执行指定的sql文件 nginx安装与部署 1.拉取镜像 2…

c语言:用一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。

题目 用一个宏&#xff0c;可以将一个整数的二进制位的奇数位和偶数位交换。 如&#xff1a;01&#xff0c;是1&#xff0c;交换完是10&#xff0c;是2. 思路 1.分别取出奇数位上的数字和偶数位上的数字 举个例子&#xff1a;1001 0110 1001 0110 奇…

4. 示例:更改监听端口

默认Spring Boot启动是监听在8080上的。 如果8080被使用&#xff0c;就会报以下错误。 这个时候可以更换一个新的端口。 server: port: 8180 然后再启动&#xff0c;启动成功并且绑定到端口8180。

【计算机硬件】2、指令系统、存储系统和缓存

文章目录 指令系统计算机指令的组成计算机指令执行过程指令的寻址方式&#xff08;怎么样找到操作数&#xff1f;&#xff09;1、顺序寻址2、跳跃寻址 指令操作数的寻址方式&#xff08;怎么样找到操作数&#xff1f;&#xff09;1、立即寻址方式2、直接寻址方式3、间接寻址方式…

JRT和springboot比较测试

想要战胜他&#xff0c;必先理解他。这两天系统的学习Maven和跑springboot工程&#xff0c;从以前只是看着复杂到亲手体验一下&#xff0c;亲自实践的才是更可靠的了解。 第一就是首先Maven侵入代码结构&#xff0c;代码一般要按约定搞src/main/java。如果是能严格执行测试的项…

Windows系统还原打印机系统教程

同时按下键盘WinR键&#xff0c;在运行窗口输入services.msc后点击确定或者回车。 找到Print Spooler&#xff0c;选中右键&#xff0c;点击停止。 复制地址&#xff1a;C:\Windows\System32\spool\drivers 打开我的电脑&#xff0c;复制到地址栏后回车&#xff0c;分别删除文件…

Tomcat10.X部署老版本axis2 webservice项目不生效

目录 一、使用场景 二、问题描述 三、原因排查 四、解决方案 一、使用场景 原来项目是OpenJDK8tomcat9构建&#xff0c;现在需要升级到OpenJDK17tomcat10的组合。原来的webservice项目打包成aar格式&#xff0c;通过axis2部署在tomcat上。 二、问题描述 在配置好jdk和to…

android 自定义八边形进度条

自定义八边形动画效果图如下 绘制步骤&#xff1a; 1.先绘制橙色底部八边形实心 2.黑色画笔绘制第二层&#xff0c;让最外层显示一条线条宽度即可 3.再用黄色画笔绘制黄色部分 4.使用渐变画笔根据当前进度绘制覆盖黄色部分 5.使用黑色画笔根据当前进度绘制刻度条 6.黑色画笔绘制…

SQL-窗口函数

什么是窗口函数 可以像聚合函数一样对一组数据进行分析并返回结果&#xff0c;二者的不同之处在于&#xff0c;窗口函数不是将一组数据汇总成单个结果&#xff0c;而是为每一行数据都返回一个结果。 窗口函数组成部分 1.创建数据分区 窗口函数OVER子句中的PARTITION BY选项用…

【Flutter 问题系列第 80 篇】TextField 输入框组件限制可输入的最大长度后,输入的内容中包含表情符号时,获取输入的内容数还是会超出限制的问题

这是【Flutter 问题系列第 80 篇】&#xff0c;如果觉得有用的话&#xff0c;欢迎关注专栏。 博文当前所用 Flutter SDK&#xff1a;3.10.5、Dart SDK&#xff1a;3.0.5 一&#xff1a;问题描述 在输入用户名称、简介等内容时&#xff0c;一般我们都会限制输入框内最大可输入…

深入解析JavaScript的原生原型

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》、《krpano中文文档》 ​ ​ ✨ 前言 在JavaScript中,除了自定义对象,还存在很多由JavaScript语言本身提供…