OnRemeasuredModifier

113 阅读3分钟

前言

传统 View 系统中,onMeasure() 是用做测量视图组件(View)及其内容的,以确定它应该占据多大的空间。我们重写它,无非有两个目的:1. 定制 View 的测量算法 2. 测量完成后,获取一个回调

其中在测量的回调中,我们可以获取组件的尺寸信息进行后续操作。

而 Jetpack Compose 中的 OnRemeasuredModifier 瞄准的就是第二点,当 OnRemeasuredModifier 所修饰的组件或者是布局节点被重新测量后,其内部的 onRemeasured() 方法就会被调用。

interface OnRemeasuredModifier : Modifier.Element {
    /**
     * Called after a layout's contents have been remeasured.
     */
    // 在布局内容重新测量之后调用 
    fun onRemeasured(size: IntSize)
}

写法

只要使用 onSizeChanged() 修饰符,就行了。

Text("Hello OnRemeasuredModifier.", Modifier.onSizeChanged {
    println("Text组件被测量了!")
})

但是 onSizeChanged 修饰符只有重新测量后,尺寸发生了改变,才会执行这个回调,它是一个优化的 onRemeasured()。当然初次测量时也会触发回调。

进入它的源码:

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

    ...
}

可以看到 onSizeChanged 修饰符确实实现了 OnRemeasuredModifier 接口,并且它重写了 onRemeasured() 方法, 只有当前尺寸和上一次的尺寸不相等,才执行 onSizeChanged 回调。

当然如果你有监听每一次重新测量的需求,也可以自己实现 OnRemeasuredModifier 接口,像这样:

Modifier.then(object : OnRemeasuredModifier {
    override fun onRemeasured(size: IntSize) {
        // 在这里去写每一次重新测量后的逻辑
        
    }
})

原理

了解了 OnRemeasuredModifier 的作用和写法,现在来看看它的原理。

如果不清楚 Modifier 是在哪被保存的,可以看看Modifier是怎么工作的?这篇文章。

它会被保存到 NodeChain 实例所维护的 Modifier.Node 双向链表中,然后它是怎么生效的?

它的回调函数 onRemeasured() 会在 BackwardsCompatNode 类中被使用到:

Ctrl + B 快捷键查看用法,点击设置,将查找范围设为 All Places——所有地方。

// BackwardsCompatNode.kt
override fun onRemeasured(size: IntSize) {
    val element = element
    if (element is OnRemeasuredModifier) {
        element.onRemeasured(size)
    }
}

BackwardsCompatNode 类的 onRemeasured() 函数会在 NodeCoordinator 类中被调用:

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

NodeCoordinator 类的 onRemeasured() 函数又会在它的子类中被调用,分别是 LayoutModifierNodeCoordinatorInnerNodeCoordinator

  • InnerNodeCoordinator:用于测量Composable函数本身的NodeCoordinator。
  • LayoutModifierNodeCoordinator:用于测量LayoutModifierNode的NodeCoordinator。

那我们分别点进去看看:

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

    measureResult = with(layoutNode.measurePolicy) {
        measure(layoutNode.childMeasurables, constraints)
    }
    onMeasured()
    this
}
// LayoutModifierNodeCoordinator.kt
override fun measure(constraints: Constraints): Placeable {
    performingMeasure(constraints) {
        with(layoutModifierNode) {
            measureResult = if (this is IntermediateLayoutModifierNode) {
                intermediateMeasure(
                    wrappedNonNull,
                    constraints,
                    lookaheadDelegate!!.measureResult.let { IntSize(it.width, it.height) },
                    lookaheadConstraints!!
                )
            } else {
                measure(wrappedNonNull, constraints)
            }
            this@LayoutModifierNodeCoordinator
        }
    }
    onMeasured()
    return this
}

其实代码很直白,就是在测量自身完成后,调用了一下 onMeasured()

现在我问你,下面这段代码中,onRemeasured() 方法会在什么时候被调用,以及它参数的 size 指的又是谁的尺寸?

Box(
    Modifier
        .size(200.dp)
        .padding(20.dp)
        .then(object : OnRemeasuredModifier {
            override fun onRemeasured(size: IntSize) {
                TODO("Not yet implemented")
            }
        })
        .padding(40.dp)
        .background(Color.Green)
)

很简单,会在 padding(40.dp) 所创建的 LayoutModifierNode 完成了测量后被调用,并且尺寸也是padding(40.dp) 所创建的LayoutModifierNode 的尺寸。(尺寸大小是 160 x 160 dp)

因为 OnRemeasuredModifierpadding(40.dp) 创建的 LayoutModifierNode 共用一个 NodeCoordinator