Compose编程思想 -- Compose中的经典Modifier(LayoutModifier)

683 阅读17分钟

前言

在上一节中,介绍了Modifier的基础知识,从本节开始将会陆陆续续介绍Compose中一些经典的Modifier使用及其原理,帮助我们在之后的自定义view通过理论基础。

1 LayoutModifier基础使用

从字面意思上来看,LayoutModifier与布局相关,在传统的View体系中,在自定义ViewGroup时,必须要重写onLayout方法,用于摆放子view的位置。那么LayoutModifier其实也是来做这件事的。

我们之前在使用Modifier的时候,像设置宽高、padding等,但凡涉及到大小修改的,都是LayoutModifier在做处理,例如我们设置宽高,会融合一个SizeModifier

@Stable
fun Modifier.size(size: Dp) = this.then(
    SizeModifier(
        minWidth = size,
        maxWidth = size,
        minHeight = size,
        maxHeight = size,
        enforceIncoming = true,
        inspectorInfo = debugInspectorInfo {
            name = "size"
            value = size
        }
    )
)

SizeModifier就是实现了LayoutModifier接口,它也是一个Element,符合我们之前的结论,任意Modifier都直接或者间接实现了Element接口。

@JvmDefaultWithCompatibility
interface LayoutModifier : Modifier.Element {
    /**
     * The function used to measure the modifier. The [measurable] corresponds to the
     * wrapped content, and it can be measured with the desired constraints according
     * to the logic of the [LayoutModifier]. The modifier needs to choose its own
     * size, which can depend on the size chosen by the wrapped content (the obtained
     * [Placeable]), if the wrapped content was measured. The size needs to be returned
     * as part of a [MeasureResult], alongside the placement logic of the
     * [Placeable], which defines how the wrapped content should be positioned inside
     * the [LayoutModifier]. A convenient way to create the [MeasureResult]
     * is to use the [MeasureScope.layout] factory function.
     *
     * A [LayoutModifier] uses the same measurement and layout concepts and principles as a
     * [Layout], the only difference is that they apply to exactly one child. For a more detailed
     * explanation of measurement and layout, see [MeasurePolicy].
     */
    fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult

    /**
     * The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth].
     */
    fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = MeasuringIntrinsics.minWidth(
        this@LayoutModifier,
        this,
        measurable,
        height
    )

    /**
     * The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight].
     */
    fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ): Int = MeasuringIntrinsics.minHeight(
        this@LayoutModifier,
        this,
        measurable,
        width
    )

    /**
     * The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth].
     */
    fun IntrinsicMeasureScope.maxIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = MeasuringIntrinsics.maxWidth(
        this@LayoutModifier,
        this,
        measurable,
        height
    )

    /**
     * The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight].
     */
    fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ): Int = MeasuringIntrinsics.maxHeight(
        this@LayoutModifier,
        this,
        measurable,
        width
    )
}

1.1 Modifier.layout使用

/**
 * Creates a [LayoutModifier] that allows changing how the wrapped element is measured and laid out.
 *
 * This is a convenience API of creating a custom [LayoutModifier] modifier, without having to
 * create a class or an object that implements the [LayoutModifier] interface. The intrinsic
 * measurements follow the default logic provided by the [LayoutModifier].
 *
 * Example usage:
 *
 * @sample androidx.compose.ui.samples.ConvenienceLayoutModifierSample
 *
 * @see androidx.compose.ui.layout.LayoutModifier
 */
fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutModifierElement(measure)

按照惯例,看下官方的解释:

Modifier.layout用于创建一个LayoutModifier,并允许改变被包装的元素的测量和布局方式

所以,Modifier.layout的核心作用就是用来测量和布局用的,它会创建一个LayoutModifier与之前 的Modifier融合,既然layout函数能够改变布局的方式,那么我们可以试一下。

首先在使用Modifier.layout时,有一个回调函数measure,其中两个参数MeasurableConstraints

/**
 * A part of the composition that can be measured. This represents a layout.
 * The instance should never be stored.
 */
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
}

其中Measurable可以认为是一个布局,甚至你可以认为它就是调用这个函数的组件。 它内部有一个函数measure,用于测量这个布局,返回值为Placeable,其内部存储了布局的size以及操作子view位置的能力,通过Placeable可以拿到布局的大小。

另一个参数Constraints,可以认为是约束条件,如同传统View体系中的ConstraintsLayout,用于控制组件的大小,例如设置最大宽度、最大高度等等,在测量的过程中会因为这些约束条件从而改变布局的方式,例如Text设置了宽度为300,但是maxWidth为200,那么最终Text的宽度就是200.

/**
 * Interface holding the size and alignment lines of the measured layout, as well as the
 * children positioning logic.
 * [placeChildren] is the function used for positioning children. [Placeable.placeAt] should
 * be called on children inside [placeChildren].
 * The alignment lines can be used by the parent layouts to decide layout, and can be queried
 * using the [Placeable.get] operator. Note that alignment lines will be inherited by parent
 * layouts, such that indirect parents will be able to query them as well.
 */
interface MeasureResult {
    val width: Int
    val height: Int
    val alignmentLines: Map<AlignmentLine, Int>
    fun placeChildren()
}

measure回调的返回值是MeasureResult,他也是一个接口,用来持有测量之后的布局宽高以及alignmentLines基线对齐线,还有一个函数placeChildren,它是用来摆放组件的位置,Placeable.placeAt函数只能在placeChildren中执行。

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
        )
    }
}

当然,我们在measure回调不需要自己创建MeasureResult接口实例对象,Compose中提供了layout函数,这个函数返回值就是MeasureResult接口实例对象,这个函数第4个参数placementBlock同样可以用来处理view的放置逻辑。

@Composable
fun ModifierLayout() {

    Text(
        text = "Layout",
        color = Color.Red,
        fontWeight = FontWeight(600),
        fontSize = 20.sp,
        modifier = Modifier.layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            Log.d("TAG", "ModifierLayout: width=${placeable.width} height=${placeable.height}")
            layout(placeable.width,placeable.height){
                //处理子view的摆放逻辑
            }
        })

}

如果是这样直接运行,Text是无法展示出来的,因为现在我们添加了Modifier.layout属性,相当于接管了系统的layout操作,因此必须要设置Text的摆放逻辑。

@Composable
fun ModifierLayout() {

    Text(
        text = "Layout",
        color = Color.Red,
        fontWeight = FontWeight(600),
        fontSize = 20.sp,
        modifier = Modifier.layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            Log.d("TAG", "ModifierLayout: width=${placeable.width} height=${placeable.height}")
            layout(placeable.width,placeable.height){
                placeable.placeRelative(0,0)
            }
        })

}

这里我们将view的位置摆放在屏幕左上角,运行之后就正常显示出来。

其实从整个Modifier.layout看下来,其实跟传统的自定义View还是挺像的:

graph LR
Modifier.layout --> Measurable.measure --> MeasureScope.layout --> Placeable.placexxx

也是一个measure到layout的流程,但是在Compose中的layout,准确的来说是Modifier中的layout,仅仅只能修改当前组件的大小以及摆放位置;如果想实现ViewGroup中的onLayout,即多个组件的摆放Logic,需要使用Layout。

关于Layout的使用,会在自定义view的章节中完整介绍。

1.2 Placeable介绍

接下来看一下Placeable这个类,看下官方解释:

Placeable是Measurable.measure的结果,主要用于通过其父布局定子组件的位置

总体来说,Placeable就是一个用来定位组件位置的类,相当于传统view中layout的作用。

/**
 * A [Placeable] corresponds to a child layout that can be positioned by its
 * parent layout. Most [Placeable]s are the result of a [Measurable.measure] call.
 *
 * A `Placeable` should never be stored between measure calls.
 */
abstract class Placeable : Measured {
    // 组件的宽度
    var width: Int = 0
        private set

    // 组件的高度
    var height: Int = 0
        private set
        
// ......

abstract class PlacementScope {

    fun Placeable.placeRelative(position: IntOffset, zIndex: Float = 0f) =
        placeAutoMirrored(position, zIndex, null)

    
    fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f) =
        placeAutoMirrored(IntOffset(x, y), zIndex, null)

    
    fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) =
        placeApparentToRealOffset(IntOffset(x, y), zIndex, null)

    
    fun Placeable.place(position: IntOffset, zIndex: Float = 0f) =
        placeApparentToRealOffset(position, zIndex, null)

    // ......
        
}

对于Placeable可以获取组件的宽高就不说了,看下PlacementScope中的几个函数,均为Placeable中的扩展函数:

  • placeRelative

placeRelative带有相对的含义,坐标x,y位置是相对于父布局的左上角,而非屏幕的左上角,而且如果父布局的方向是从右向左,那么使用placeRelative也会镜像地摆放在父布局的右上角。

  • place

place则是一个没有相对位置概念的函数,坐标x,y位置同样是相对于父布局的左上角,而非屏幕的左上角,但如果父布局的方向是从右向左,那么使用placeRelative依然会将组件摆放在左上角,不会做镜像处理。

所以两者区别就是是否适应RTL布局。

1.3 使用场景

从前面的介绍中,我们大概知道,使用Modifier的layout函数可用于修改组件的尺寸,或者组件的摆放位置,仅此而已,像Text组件中的文字摆放顺序,布局容器中的组件摆放顺序,使用layout是完全无法干预的。 因此layout仅仅是对组件的大小和位置做装饰.

@Composable
fun ModifierLayout() {

    Row(
        modifier = Modifier
            .background(Color.Green)
            .width(200.dp)
    ) {
        Text(
            text = "Layout",
            color = Color.Red,
            fontWeight = FontWeight(600),
            fontSize = 20.sp,
            modifier = Modifier.layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
                val maxSize = maxOf(placeable.width, placeable.height)
                layout(maxSize, maxSize) {
                    placeable.placeRelative(0, maxSize / 4)
                }
            })
    }
}

例如在对Text测量完成之后,希望将组件的宽高设置为Text的宽高最大值,并且摆放位置希望居中,这个时候就可以使用Modifier的layout函数,而如果想要进一步修改Text内部的具体布局实现,请使用Layout函数。

2 LayoutModifier底层原理

在前面介绍Modifier.layout时说过,它会创建一个LayoutModifier对象。

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

从源码看到,最终创建的是LayoutModifierElement,他其实就是一个LayoutModifier

@OptIn(ExperimentalComposeUiApi::class)
private data class LayoutModifierElement(
    val measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) : ModifierNodeElement<LayoutModifierImpl>() {
    override fun create() = LayoutModifierImpl(measure)

    override fun update(node: LayoutModifierImpl) = node.apply {
        measureBlock = measure
    }

    override fun InspectorInfo.inspectableProperties() {
        name = "layout"
        properties["measure"] = measure
    }
}

回到Compose的整体,当我们在创建页面布局时,会通过一系列的@Composable函数组合,例如:

Column(modifier = Modifier.background(Color.Green)) {
    Text(text = "文本", modifier = Modifier.clickable {  })
    Box {
        Button(onClick = { /*TODO*/ }) {
            Text(text = "按钮")
        }
    }
}

每个@Composable组件在Compose的UI体系中,都对应一个LayoutNode,也就是说Compose的布局体系中,Compose的界面树其实是一棵LayoutNode树,在measure和layout的时候,操控的就是LayoutNode。

stateDiagram-v2



Column --> Text
Column --> Box
Box --> Button


LayoutNode(Column) --> LayoutNode(Text)
LayoutNode(Column) --> LayoutNode(Box)
LayoutNode(Box) --> LayoutNode(Button)

在每一个LayoutNode中,都有一个modifier成员变量,这个Modifier其实就是我们在Column或者Box中定义的Modifier。

/**
 * The [Modifier] currently applied to this node.
 */
override var modifier: Modifier = Modifier
    set(value) {
        require(!isVirtual || modifier === Modifier) {
            "Modifiers are not supported on virtual LayoutNodes"
        }
        field = value
        // 更新链表数据
        nodes.updateFrom(value)

        // TODO(lmr): we don't need to do this every time and should attempt to avoid it
        //  whenever possible!
        forEachCoordinatorIncludingInner {
            it.updateLookaheadScope(mLookaheadScope)
        }

        layoutDelegate.updateParentData()
    }

当我们设置Modifier的时候,其实就是执行了LayoutNodemodifier的set函数。在set方法中,会执行一个非常重要的操作,就是执行NodeChainupdateFrom函数,从字面意思上看就是根据设置的Modifier更新LayoutNode节点的Modifier

2.1 NodeChain

先看下NodeChain的源码,在NodeChain的构造方法中,有一个LayoutNode成员变量,可以认为就是Compose树中的每一个组件,或者具体来说就是每一个View,可以看下本小节开头的关系图:

stateDiagram-v2

LayoutNode(Column) --> LayoutNode(Text)
LayoutNode(Column) --> LayoutNode(Box)
LayoutNode(Box) --> LayoutNode(Button)

NodeChain中持有一个Compose组件的引用,看下几个关键的成员变量:

  • innerCoordinator

    可以认为是每个LayoutNode中的Modifier.Node的统筹管理者,Modifier.Node可以认为是Modifier.Element转换后的结果,属于在Modifier链中的节点。

  • tailhead

    从字面意思上看,就是Modifier.Node的头部和尾部,这里头部和尾部指针,都指向了尾部,所以NodeChain是一个双向链表。

internal class NodeChain(val layoutNode: LayoutNode) {
    internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
    internal var outerCoordinator: NodeCoordinator = innerCoordinator
        private set
    // 
    internal val tail: Modifier.Node = innerCoordinator.tail
    internal var head: Modifier.Node = tail
        private set
    private val isUpdating: Boolean get() = head === SentinelHead
    private val aggregateChildKindSet: Int get() = head.aggregateChildKindSet
    private var current: MutableVector<Modifier.Element>? = null
    private var buffer: MutableVector<Modifier.Element>? = null
    private var cachedDiffer: Differ? = null
    private var logger: Logger? = null

    internal fun useLogger(logger: Logger?) {
        this.logger = logger
    }

    private fun padChain() {
        check(head !== SentinelHead)
        val currentHead = head
        currentHead.parent = SentinelHead
        SentinelHead.child = currentHead
        head = SentinelHead
    }

    private fun trimChain() {
        check(head === SentinelHead)
        head = SentinelHead.child ?: tail
        head.parent = null
        SentinelHead.child = null
        check(head !== SentinelHead)
    }

    /**
     * This method will update the node chain based on the provided modifier chain. This method is
     * responsible for calling all appropriate lifecycles for nodes that are
     * created/disposed/updated during this call.
     *
     * This method will attempt to optimize for the common scenario of the modifier chain being of
     * equal size and each element being able to be reused from the prior one. In most cases this
     * is what recomposition will result in, provided modifiers weren't conditionally provided. In
     * the cases where the modifier is not of equal length to the prior value, or modifiers of
     * different reuse types ended up in the same position, this method will deopt into a slower
     * path which will perform a diff on the modifier chain and execute a minimal number of
     * insertions and deletions.
     */
    internal fun updateFrom(m: Modifier) {
        // If we run the diff and there are no new nodes created, then we don't need to loop through
        // and run the attach cycle on them. We simply keep track of this during the diff to avoid
        // this overhead at the end if we can, since it should be fairly common.
        var attachNeeded = false
        // If we run the diff and there are no structural changes, we can avoid looping through the
        // list and updating the coordinators. We simply keep track of this during the diff to avoid
        // this overhead at the end if we can, since it should be fairly common. Note that this is
        // slightly different from [attachNeeded] since a node can be updated and return null or a
        // new instance which is perfectly valid and would require a new attach cycle, however the
        // coordinator would be identical and so [attachNeeded] would be true but this false
        var coordinatorSyncNeeded = false
        // Use the node chain itself as a head/tail temporarily to prevent pruning the linkedlist
        // to the point where we don't have reference to it. We need to undo this at the end of
        // this method.
        padChain()
        
        // 第一步:创建两个数组
        val before = current ?: MutableVector(capacity = 0)
        val after = m.fillVector(buffer ?: mutableVectorOf())
        if (after.size == before.size) {
            // assume if the sizes are the same, that we are in a common case of no structural
            // changes we will attempt an O(n) fast-path diff and exit if a diff is detected, and
            // do the O(N^2) diff beyond that point
            val size = before.size
            // for the linear diff we want to start with the "unpadded" tail
            var node: Modifier.Node? = tail.parent
            var i = size - 1
            var aggregateChildKindSet = 0
            while (node != null && i >= 0) {
                val prev = before[i]
                val next = after[i]
                when (actionForModifiers(prev, next)) {
                    ActionReplace -> {
                        // TODO(lmr): we could avoid running the diff if i = 0, since that would
                        //  always be simple remove + insert
                        // structural change!
                        // back up one for the structural diff algorithm. This should be safe since
                        // our chain is padded with the EmptyHead/EmptyTail nodes
                        logger?.linearDiffAborted(i, prev, next, node)
                        i++
                        node = node.child
                        break
                    }
                    ActionUpdate -> {
                        // this is "the same" modifier, but some things have changed so we want to
                        // reuse the node but also update it
                        val beforeUpdate = node
                        node = updateNodeAndReplaceIfNeeded(prev, next, beforeUpdate)
                        logger?.nodeUpdated(i, i, prev, next, beforeUpdate, node)
                    }
                    ActionReuse -> {
                        logger?.nodeReused(i, i, prev, next, node)
                        // no need to do anything, this is "the same" modifier
                    }
                }
                // if the node is new, we need to run attach on it
                if (!node.isAttached) attachNeeded = true

                aggregateChildKindSet = aggregateChildKindSet or node.kindSet
                node.aggregateChildKindSet = aggregateChildKindSet

                node = node.parent
                i--
            }

            if (i > 0) {
                check(node != null)
                attachNeeded = true
                coordinatorSyncNeeded = true
                // there must have been a structural change
                // we only need to diff what is left of the list, so we use `i` as the "beforeSize"
                // and "afterSize"
                structuralUpdate(
                    before,
                    i,
                    after,
                    i,
                    // its important that the node we pass in here has an accurate
                    // "aggregateChildMask"
                    node,
                )
            }
        } else if (before.size == 0) {
            // common case where we are initializing the chain and the previous size is zero. In
            // this case we just do all inserts. Since this is so common, we add a fast path here
            // for this condition.
            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)
                logger?.nodeInserted(0, i, next, child, node)
                aggregateChildKindSet = aggregateChildKindSet or node.kindSet
                node.aggregateChildKindSet = aggregateChildKindSet
                i--
            }
        } else if (after.size == 0) {
            // common case where we we are removing all the modifiers.
            coordinatorSyncNeeded = true
            var i = before.size - 1
            // for the linear traversal we want to start with the "unpadded" tail
            var node: Modifier.Node? = tail.parent
            while (node != null && i >= 0) {
                logger?.nodeRemoved(i, before[i], node)
                val parent = node.parent
                detachAndRemoveNode(node)
                node = parent
                i--
            }
        } else {
            attachNeeded = true
            coordinatorSyncNeeded = true
            structuralUpdate(
                before,
                before.size,
                after,
                after.size,
                tail,
            )
        }
        current = after
        // clear the before vector to allow old modifiers to be Garbage Collected
        buffer = before.also { it.clear() }
        trimChain()

        if (coordinatorSyncNeeded) {
            syncCoordinators()
        }
        if (attachNeeded && layoutNode.isAttached) {
            attach(performInvalidations = true)
        }
    }

    // .....
}

看一下updateFrom函数,假设目前组件的Modifier如下:

Modifier
    .background(LocalBackground.current)
    .size(10.dp)
    .align(Alignment.Center)
graph LR
background --> size --> align

(1)创建2个数组beforeaftercurrent是指当前LayoutNodeModifier.Node合集,如上述流程图所示,如果是第一次加载该组件,那么默认就是空的;after是指对当前传入的Modifier做了一次处理,看下fillVector函数。

private fun Modifier.fillVector(
    result: MutableVector<Modifier.Element>
): MutableVector<Modifier.Element> {
    // 创建一个数组,先将传入的Modifier加进去。
    val stack = MutableVector<Modifier>(result.size).also { it.add(this) }
    while (stack.isNotEmpty()) {
        when (val next = stack.removeAt(stack.size - 1)) {
            is CombinedModifier -> {
                stack.add(next.inner)
                stack.add(next.outer)
            }
            is Modifier.Element -> result.add(next)
            // some other androidx.compose.ui.node.Modifier implementation that we don't know about...
            else -> next.all {
                result.add(it)
                true
            }
        }
    }
    return result
}

在这个函数中会遍历所有的Modifier.Element,如果是CombinedModifier类型,这种是最常见的,在Modifier链式调用中,都会通过CombinedModifier来融合,注意stack中加入的顺序,是inner先加入,outer后加入,即新的Modifier先进,调用方后进入,然后在取元素的时候,先从栈底取,也就是普通的Modifier.Element,其实fillVector等效于Modifier.ElementfoldIn顺序遍历,将Modifier(可能存在嵌套)平铺展开

现在我们大概清楚beforeafter两个数组的含义,分别代表:当前LayoutNode的modifier element数组以及传入的的Modifier element数组

(2)接下来分3种情况:

  • before和after的size一致,说明之前已经给组件设置过Modifier,而且数量级没有发生改变,因此只需要处理更新、替换、复用的操作即可,这里我就不再做过多的赘述了;

  • 当before为空时,这是我要重点讲的,因为这是第一次设置Modifier时流程。此时在after数组中,存储的顺序为:

    graph LR
    background --> size --> align
    

    在初始化中的流程,是从after数组中最后一个元素开始遍历,也就是align属性,

    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)
       logger?.nodeInserted(0, i, next, child, node)
       aggregateChildKindSet = aggregateChildKindSet or node.kindSet
       node.aggregateChildKindSet = aggregateChildKindSet
       i--
    }
    

    每次创建都是执行createAndInsertNodeAsParent函数,在这个函数中会判断当前Modifier.Element的属性,如果是ModifierNodeElement,例如LayoutModifier,那么就执行其create函数,创建LayoutModifierImpl;如果是普通的Modifier.Element,则将其封装为BackwardsCompatNode.

    private fun createAndInsertNodeAsParent(
        element: Modifier.Element,
        child: Modifier.Node,
    ): Modifier.Node {
        val node = when (element) {
            is ModifierNodeElement<*> -> element.create().also {
                it.kindSet = calculateNodeKindSetFrom(it)
            }
            else -> BackwardsCompatNode(element)
        }
        check(!node.isAttached)
        node.insertedNodeAwaitingAttachForInvalidation = true
        return insertParent(node, child)
    }
    

    上述的过程,其实就是将Modifier Element转换为Modifier Node的过程。 然后执行insertParent,我们关注下两个入参:

    node:Modifier.Node,其实就是实例化的过程,对应了ModifierNodeElement和普通的Modifier.Element

    child:第一次传进来的是tail,也就是innerCoordinator的tail,可以认为就是组件自身。

    添加的过程属于头插法, tail的父节点设置为了传进来的node,也就是algin属性,后续进来的Modifier.Node将会继续头插,直到遍历完成。

    /**
     * This inserts [node] as the parent of [child] in the current linked list.
     * For example:
     *
     *      Head... -> child -> ...Tail
     *
     *  gets transformed into a list of the following shape:
     *
     *      Head... -> node -> child -> ...Tail
     *
     *  @return The inserted [node]
     */
    private fun insertParent(node: Modifier.Node, child: Modifier.Node): Modifier.Node {
        val theParent = child.parent
        if (theParent != null) {
            theParent.child = node
            node.parent = theParent
        }
        // 核心代码:
        child.parent = node
        node.child = child
        return node
    }
    

看一下下面的图,我们梳理下整个初始化的流程:

Modifier
    .background(LocalBackground.current)
    .size(10.dp)
    .align(Alignment.Center)

Modifier.fillVector过程:

graph LR
background --> size --> align

头插的过程:

image.png

2.2 同步过程

在初始化完成之后,因为标志位coordinatorSyncNeeded设置为了true,因此需要进行同步的过程,在这个函数中:

coordinator:其实对应的是innerCoordinator,可以理解为组件自身,因为它是在NodeChain中初始化的,每个LayoutNode都有一个NodeChain

node:它是tail的parent节点,从2.1小节末尾的图可以知道,就是aligin对应的Modifier.Node.

private fun syncCoordinators() {
    var coordinator: NodeCoordinator = innerCoordinator
    // tail尾部节点的parent就是
    var node: Modifier.Node? = tail.parent
    while (node != null) {
        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
            }
            // 关注下核心代码
            // wrappedBy指的是外部的coordinator
            coordinator.wrappedBy = next
            // wrapped指的是内部的coordinator
            next.wrapped = coordinator
            coordinator = next
        } else {
            node.updateCoordinator(coordinator)
        }
        node = node.parent
    }
    coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
    // 同步完成之后,将outerCoordinator的值设置为了coordinator
    outerCoordinator = coordinator
}

如果当前Node为LayoutModifierNode,那么会将Modifier.Node包上一层NodeCoordinator,具体怎么包的,看下核心代码:

coordinator就是innerCoordinator,它被next也就是LayoutModifierNodeCoordinator包裹,next包裹着innerCoordinator,然后将coordinator指向LayoutModifierNodeCoordinator

如果当前Modifier不是LayoutModifierNode,那么会共用上一个LayoutModifierNodeCoordinator,因为只是调用了更新函数updateCoordinator ,例如DrawModifier或者ParentDataModifier,之所以这么做是因为在layoutdraw的时候需要做区分。

image.png

在同步完成之后,会将coordinatorouterCoordinator赋值,也就是说将最外层的LayoutModifierNodeCoordinator给予outerCoordinator 这里需要关注下wrappedwrappedBy对象,后续源码介绍会用到。

2.3 remeasure

在设置完成Modifier之后,接下来就需要根据这些配置信息测量组件的大小以及摆放位置,此时会执行到LayoutNode的remeasure函数。

/**
 * Return true if the measured size has been changed
 */
internal fun remeasure(
    constraints: Constraints? = layoutDelegate.lastConstraints
): Boolean {
    return if (constraints != null) {
        if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
            // This LayoutNode may have asked children for intrinsics. If so, we should
            // clear the intrinsics usage for everything that was requested previously.
            clearSubtreeIntrinsicsUsage()
        }
        measurePassDelegate.remeasure(constraints)
    } else {
        false
    }
}

最终执行的是outerCoordinator的measure函数,在2.2小节中,我们介绍过outerCoordinator其实就是最外层的LayoutModifierNodeCoordinator

/**
 * Performs measure with the given constraints and perform necessary state mutations before
 * and after the measurement.
 */
private fun performMeasure(constraints: Constraints) {
    check(layoutState == LayoutState.Idle) {
        "layout state is not idle before measure starts"
    }
    layoutState = LayoutState.Measuring
    measurePending = false
    layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
        layoutNode,
        affectsLookahead = false
    ) {
         // 这里做真正的测量
        outerCoordinator.measure(constraints)
    }
    // The resulting layout state might be Ready. This can happen when the layout node's
    // own modifier is querying an alignment line during measurement, therefore we
    // need to also layout the layout node.
    if (layoutState == LayoutState.Measuring) {
        markLayoutPending()
        layoutState = LayoutState.Idle
    }
}

真正做测量的就是LayoutModifierNodeCoordinator的measure函数。

// LayoutModifierNodeCoordinator.kt 

override fun measure(constraints: Constraints): Placeable {
    performingMeasure(constraints) {
        with(layoutModifierNode) { // scope:LayoutModifierNode
            measureResult = measure(wrappedNonNull, constraints)
            this@LayoutModifierNodeCoordinator
        }
    }
    onMeasured()
    return this
}

在measure中,通过with函数提供了LayoutModifierNode上下文,其实在LayoutModifier在转换的过程中会创建LayoutModifierImpl,它其实就是一个Modifier.Node,会执行其measure函数,其实最终还是执行了block中的函数,也就是说执行了我们自定义的测量流程,所以这样就能够影响系统底层测量的过程。

@OptIn(ExperimentalComposeUiApi::class)
internal class LayoutModifierImpl(
    var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult
) : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ) = measureBlock(measurable, constraints)

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

2.3 Modifier的顺序敏感

首先,我们在前面的介绍中,在同步Coordinator的时候,会从tail开始,依次拿到其parent,判断是否为layout类型,如果是就会构建LayoutModifierNodeCoordinator;如果不是layout类型,那么就会复用上一个LayoutModifierNodeCoordinator

如下面这个例子,我以Text中的Modifier为例:

Box(
    Modifier
        .background(Color.Blue)
        .size(100.dp)
) {

    Text(text = "按钮",
        Modifier
            .background(Color.Green)
            .padding(10.dp)
            .clickable {

            }
            .padding(20.dp)
            .align(Alignment.Center)
    )

}

tail就是Text,它是一个LayoutNode,同时内部拥有一个NodeChain,那么开始同步的时候,从align开始,因为padding(20.dp)属于LayoutModifier,那么会新建一个LayoutModifierNodeCoordinatorclickable不属于layout类型,那么就会复用padding(20.dp),接着padding(10.dp)又会新建一个LayoutModifierNodeCoordinator

所以在测量的时候,outerCoordinator就是padding(10.dp)对应的NodeCoordinator,那么这块区域不会被设置监听;而测量到padding(20.dp)时,会发现clickable也复用了这块区域,那么点击事件的响应也只会加到20dp上,而不会加到后续的10dp范围上,例如下图展示:

Screenshot_20240325_153639.png

如果想要全部响应,那么clickable事件需要在所有的padding事件设置之前就定义好。