Compose编程思想 -- Compose中可以监听组件变化的Modifier

1,208 阅读6分钟

前言

在前面的文章里,我介绍了LayoutModifierDrawModifier在Compose测量布局绘制的过程中的作用,介绍了ParentDataModifier,用于提供数据给父容器做布局逻辑处理,那么本节我将会介绍组件测量过程中发生变化时,如何能够监听到。

1 OnRemeasureModifier

惯例还是看官方解释:

当布局的内容被重新测量的时候会被调用,通常会使用onSizeChanged来监听

/**
 * A modifier whose [onRemeasured] is called when the layout content is remeasured. The
 * most common usage is [onSizeChanged].
 *
 * Example usage:
 * @sample androidx.compose.ui.samples.OnSizeChangedSample
 */
@JvmDefaultWithCompatibility
interface OnRemeasuredModifier : Modifier.Element {
    /**
     * Called after a layout's contents have been remeasured.
     */
    fun onRemeasured(size: IntSize)
}

其实就是在组件的内容,例如宽高发生变化的时候,onRemeasured函数会被重新调用,此时会拿到一个新的组件状态值IntSize,里面包含了宽高信息。

1.1 OnRemeasuredModifier使用

在官方文档中,建议我们使用onSizeChanged监听,例如点击一个按钮之后,将Text的宽度改变,看onSizeChanged是否会被回调。

var textSize by remember {
    mutableStateOf(200.dp)
}
Column {

    Text(text = "Text", modifier = Modifier
        .width(textSize)
        .background(Color.Red)
        .onSizeChanged {
            Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
        })
    Button(onClick = {
        textSize = 250.dp
    }) {
        Text(text = "点击改变Text宽度")
    }

}

经过测试,初始化和点击按钮时,都会回调onSizeChanged

com.lay.composestudy                 D  onCreate: onSizeChanged 688

其实onSizeChanged调用时,会创建OnSizeChangedModifierOnSizeChangedModifier其实就是实现了OnRemeasuredModifier

@Stable
fun Modifier.onSizeChanged(
    onSizeChanged: (IntSize) -> Unit
) = this.then(
    OnSizeChangedModifier(
        onSizeChanged = onSizeChanged,
        inspectorInfo = debugInspectorInfo {
            name = "onSizeChanged"
            properties["onSizeChanged"] = onSizeChanged
        }
    )
)

private class OnSizeChangedModifier(
    val onSizeChanged: (IntSize) -> Unit,
    inspectorInfo: InspectorInfo.() -> Unit
) : OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
    private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)

    override fun onRemeasured(size: IntSize) {
        if (previousSize != size) {
            onSizeChanged(size)
            previousSize = size
        }
    }
    // ......
}

1.2 OnRemeasuredModifier原理分析

一旦讲到Modifier的原理,就需要再一次回顾Modifier的初始化流程。

当Modifier初始化的时候,首先会将Modifier.Element转换为Modifier.Node,采用头插法加到NodeChain的双向链表中。

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

NodeChain中的Modifier.Node中,如果是ModifierNodeElement类型的,例如LayoutModifier或者DrawModifier,就按照自身的具体实现类型创建对应的Modifier.Node;其他类型,就统一创建为BackwardsCompatNode

实例化完成之后,在LayoutNode测量布局的时候,会执行remeasure函数,拿1.1 小节这个例子:

image.png

其中OnRemeasuredModifier是依附在InnerCoordinator这个Node上,那么在remeasure函数调用的时候,会从外层的LayoutModifierNodeCoordinator开始测量(执行measure函数),一层一层,所以我这边就直接看内层的InnerNodeCoordinator中的measure函数即可。

// InnerNodeCoordinator.kt

override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
    // before rerunning the user's measure block reset previous measuredByParent for children
    layoutNode.forEachChild {
        it.measuredByParent = LayoutNode.UsageByParent.NotUsed
    }

    measureResult = with(layoutNode.measurePolicy) {
        measure(layoutNode.childMeasurables, constraints)
    }
    // 核心代码
    onMeasured()
    return this
}

看下属于这节的核心代码,onMeasured函数:

fun onMeasured() {
    if (hasNode(Nodes.LayoutAware)) {
        Snapshot.withoutReadObservation {
            visitNodes(Nodes.LayoutAware) {
                it.onRemeasured(measuredSize)
            }
        }
    }
}

首先会判断,是否存在Nodes.LayoutAware类型的节点,这个节点是什么类型?我查了一下在calculateNodeKindSetFrom函数中,并没有一个对应的OnRemeasureModifierNode

internal fun calculateNodeKindSetFrom(node: Modifier.Node): Int {
    var mask = Nodes.Any.mask
    // ......
    if (node is LayoutAwareModifierNode) {
        mask = mask or Nodes.LayoutAware
    }
}

但是,BackwardsCompatNode其实是实现了LayoutAwareModifierNode,所以OnRemeasuredModifier就是这个类型。

internal class BackwardsCompatNode(element: Modifier.Element) :
    // ......
    LayoutAwareModifierNode,

然后在onMeasured函数中,会执行onRemeasured并把重新测量的值传递过来。

当然这是一个情况,如果在外层的outerCoordinator中也有OnRemeasuredModifier,那么在测量的时候,原理其实基本是一样,所以需要注意的一点就是OnRemeasuredModifier只会回调它依附于的那一层LayoutModifierNodeCoordinator测量的变化。

Text(text = "Text", modifier = Modifier
    .width(textSize)
    .background(Color.Red)
    .padding(padding1)
    .onSizeChanged {
        Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
    }
    .padding(padding2))
Button(onClick = {
    padding1 = 30.dp
}) {
    Text(text = "点击改变Text宽度")
}

例如当padding1的值发生变化时,onSizeChanged是不会有回调的结果过来的,除非发生了重组。

2 OnPlacedModifier

官方解释:

onPlaced函数的调用时机是在父容器摆放完成之后,任意子组件摆放之前调用

@JvmDefaultWithCompatibility
interface OnPlacedModifier : Modifier.Element {
    /**
     * [onPlaced] is called after parent [LayoutModifier] and parent layout gets placed and
     * before any child [LayoutModifier] is placed.
     *
     * [coordinates] provides [LayoutCoordinates] of the [OnPlacedModifier]. Placement in both
     * parent [LayoutModifier] and parent layout can be calculated using the [LayoutCoordinates].
     */
    fun onPlaced(coordinates: LayoutCoordinates)
}

准确的来说,就是父容器的LayoutModifier执行之后,在子组件的LayoutModifier执行之前会回调onPlaced函数。调用的时机在组合阶段,子组件的位置发生变化,都是进行onPlaced函数的回调。

2.1 OnPlacedModifier使用

例如对Text组件设置了onSizeChanged,onPlaced,layout,其实padding也是LayoutModifier,所以这3个函数的执行顺序为:onPlaced > onSizeChanged > layout

Text(text = "Text", modifier = Modifier
    .width(textSize)
    .background(Color.Red)
    .onSizeChanged {
        Log.d("TAG", "onCreate: onSizeChanged ${it.width}")
    }
    .onPlaced {
        Log.d("TAG", "onCreate: onPlaced ${it.parentCoordinates}")
    }
    .layout { measurable, constraints ->
        Log.d("TAG", "onCreate: call child layout")
        layout(100, 100) {

        }
    }
    .padding(padding2))

onPlaced函数的回调中,除了组件的大小之外,还可以拿到组件的位置信息,比OnRemeasureModifier拿到的信息要更多。

@Composable
fun TestOnPlaceModifier() {
    Box {
        Text(text = "文字", Modifier.size(200.dp).background(Color.Red).onPlaced {
            val positionInParent = it.positionInParent()
            if (positionInParent.x > 20 && positionInParent.y > 20){
                //TODO 动效展示,或者文字展示
            }
        })
    }
}

所以通过onPlaced拿到详细的位置信息,可以根据产品的需求做不同的效果展示。

2.2 OnPlacedModifier原理分析

在Compose测量阶段,会调用LayoutNode的remeasure函数;在摆放阶段,会执行replace函数。

/**
 * Place this layout node again on the same position it was placed last time
 */
internal fun replace() {
    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.
        clearSubtreePlacementIntrinsicsUsage()
    }
    try {
        relayoutWithoutParentInProgress = true
        // 核心代码
        measurePassDelegate.replace()
    } finally {
        relayoutWithoutParentInProgress = false
    }
}

如果看过remeasure函数,那么在replace函数中执行到核心代码时,最终会执行outerCoordinatorreplace函数。

fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) =
    placeApparentToRealOffset(IntOffset(x, y), zIndex, null)
@Suppress("NOTHING_TO_INLINE")
internal inline fun Placeable.placeApparentToRealOffset(
    position: IntOffset,
    zIndex: Float,
    noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
    placeAt(position + apparentToRealOffset, zIndex, layerBlock)
}

跟一下代码,看下placeAt函数的实现,这个是一个抽象接口,具体调用还是LayoutModifierNodeCoordinatorInnerNodeCoordinator调用这个函数。

override fun placeAt(
    position: IntOffset,
    zIndex: Float,
    layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
    super.placeAt(position, zIndex, layerBlock)
    // The coordinator only runs their placement block to obtain our position, which allows them
    // to calculate the offset of an alignment line we have already provided a position for.
    // No need to place our wrapped as well (we might have actually done this already in
    // get(line), to obtain the position of the alignment line the coordinator currently needs
    // our position in order ot know how to offset the value we provided).
    if (isShallowPlacing) return
    // 核心代码
    onPlaced()
    PlacementScope.executeWithRtlMirroringValues(
        measuredSize.width,
        layoutDirection,
        this
    ) {
        measureResult.placeChildren()
    }
}

最终执行还是调用onPlaced,因为OnPlacedModifier的类型依然是Nodes.LayoutAware,所以会调用onPlaced函数,然后会把NodeCoordinator对象传递进去,在这个对象中已经保存了Node的全部属性信息。

// NodeCoordinator.kt

@OptIn(ExperimentalComposeUiApi::class)
fun onPlaced() {
    val lookahead = lookaheadDelegate
    if (lookahead != null) {
        visitNodes(Nodes.LayoutAware) {
            it.onLookaheadPlaced(lookahead.lookaheadLayoutCoordinates)
        }
    }
    visitNodes(Nodes.LayoutAware) {
        it.onPlaced(this)
    }
}

这里需要关注一个点,在调用了OnPlaced函数之后,才调用了MeasureResultplaceChildren函数,因为在这个函数中才是真正做组件的摆放逻辑的,印证了2.1小节开头的结论。

3 OnGloballyPositionedModifier

官方解释:

当每个LayoutModifierNodeCoordinator的全局内容发生变化的时候,例如位置,大小等,onGloballyPositioned函数会被执行,这个过程会在组合完成后。

/**
 * A modifier whose [onGloballyPositioned] is called with the final LayoutCoordinates of the
 * Layout when the global position of the content may have changed.
 * Note that it will be called after a composition when the coordinates are finalized.
 *
 * Usage example:
 * @sample androidx.compose.ui.samples.OnGloballyPositioned
 */
@JvmDefaultWithCompatibility
interface OnGloballyPositionedModifier : Modifier.Element {
    /**
     * Called with the final LayoutCoordinates of the Layout after measuring.
     * Note that it will be called after a composition when the coordinates are finalized.
     * The position in the modifier chain makes no difference in either
     * the [LayoutCoordinates] argument or when the [onGloballyPositioned] is called.
     */
    fun onGloballyPositioned(coordinates: LayoutCoordinates)
}

3.1 OnGloballyPositionedModifier使用

在Modifier中提供了onGloballyPositioned函数,在调用这个函数的时候,会创建OnGloballyPositionedModifierImpl,它是继承自OnGloballyPositionedModifier

@Stable
fun Modifier.onGloballyPositioned(
    onGloballyPositioned: (LayoutCoordinates) -> Unit
) = this.then(
    OnGloballyPositionedModifierImpl(
        callback = onGloballyPositioned,
        inspectorInfo = debugInspectorInfo {
            name = "onGloballyPositioned"
            properties["onGloballyPositioned"] = onGloballyPositioned
        }
    )
)

private class OnGloballyPositionedModifierImpl(
    val callback: (LayoutCoordinates) -> Unit,
    inspectorInfo: InspectorInfo.() -> Unit
) : OnGloballyPositionedModifier, InspectorValueInfo(inspectorInfo) {
    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
        callback(coordinates)
    }
    // ......
}    

例如下面的例子:

Column {

    Text(text = "Text", modifier = Modifier
        .width(textSize)
        .background(Color.Red)
        .onGloballyPositioned {
            Log.d("TAG", "onGloballyPositioned: $it")
        }
        .padding(padding2))
    Button(onClick = {
        padding1 = 30.dp
    }) {
        Text(text = "点击改变Text宽度")
    }

}

首先,OnGloballyPositionedModifier是依附于padding(xx)这块区域的,当这块区域的位置大小发生变化的时候,就会回调onGloballyPositioned函数,等会,怎么和OnPlacedModifier是一样的?甚至返回都是一样的。

那么这两者有什么区别呢?其实onGloballyPositioned会在组件相对于窗口的位置发生变化的时候回调;而onPlaced会在组件相对于父容器的位置发生变化的时候回调;综合来说,onGloballyPositioned的回调会更加频繁一些,因此考虑到性能问题,需要谨慎使用onGloballyPositioned

3.2 OnGloballyPositionedModifier原理分析

因为OnGloballyPositionedModifier涉及到窗口的变动,因此需要从原生View开始看,就是AndroidComposedView,在measureAndLayout函数调用时,会执行measureAndLayoutDelegatedispatchOnPositionedCallbacks函数,从字面意思上看,就是进行位置分发。


// AndroidComposedView.kt 

override fun measureAndLayout(sendPointerUpdate: Boolean) {
    trace("AndroidOwner:measureAndLayout") {
        val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
        val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
        if (rootNodeResized) {
            requestLayout()
        }
        measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
    }
}

最终会执行OnPositionedDispatcherdispatch函数,因为前面的文章中我讲过,Compose的UI体系基础就是LayoutNode树,每一个组件对应一个LayoutNode,这里会遍历LayoutNode树,执行dispatchHierarchy函数。


// OnPositionedDispatcher.kt

fun dispatch() {
    // sort layoutNodes so that the root is at the end and leaves are at the front
    layoutNodes.sortWith(DepthComparator)
    layoutNodes.forEachReversed { layoutNode ->
        if (layoutNode.needsOnPositionedDispatch) {
            dispatchHierarchy(layoutNode)
        }
    }
    layoutNodes.clear()
}

最终执行到LayoutNode中的dispatchOnPositionedCallbacks函数,在这个函数中会从head遍历到tail,碰到Nodes.GlobalPositionAware类型的节点,也就是OnGloballyPositionedModifier,就会执行其onGloballyPositioned函数。

// LayoutNode.kt

@OptIn(ExperimentalComposeUiApi::class)
internal fun dispatchOnPositionedCallbacks() {
    if (layoutState != Idle || layoutPending || measurePending) {
        return // it hasn't yet been properly positioned, so don't make a call
    }
    if (!isPlaced) {
        return // it hasn't been placed, so don't make a call
    }
    nodes.headToTail(Nodes.GlobalPositionAware) {
        it.onGloballyPositioned(it.requireCoordinator(Nodes.GlobalPositionAware))
    }
}

所以从源码来看,OnPlacedModifier是在LayoutNode进行测量布局的时候调用,而OnGloballyPositionedModifier则是整个窗口测量布局之后,才会调用,后者是更加重量级的刷新。