OnGloballyPositionedModifier 详解

172 阅读3分钟

作用

来到 OnGloballyPositionedModifier 接口的源码:

interface OnGloballyPositionedModifier : Modifier.Element {
    /**
     * 在测量完成后,使用Layout的最终LayoutCoordinates调用此方法。
     * 注意,它会在组合(composition)完成后、当坐标信息最终确定时被调用。
     * 此修饰符在修饰符链中的位置不会影响传入的[LayoutCoordinates]参数,
     * 也不会影响[onGloballyPositioned]被调用的时机。
     */
    fun onGloballyPositioned(coordinates: LayoutCoordinates)
}

OnGloballyPositionedModifier 名称里的 GloballyPosition 指的是"全局定位"。在Compose的上下文中,"全局"表示相对于整个窗口(window)的坐标系统,而非仅仅相对于父组件的局部坐标系统。

当前 OnGloballyPositionedModifier 所在的 Composable 函数对应的 InnerNodeCoordinator 或者右侧最近的 LayoutModifierNode 对应的 LayoutModifierNodeCoordinator 的尺寸或者位置发生改变了 ,会触发 onGloballyPositioned() 函数的回调,当然,初始运行时也会触发。

简单来说是当前 OnGloballyPositionedModifier 所在的组件对应的布局节点在窗口中的位置或尺寸发生变化

InnerNodeCoordinator 和 LayoutModifierNodeCoordinator 都是 NodeCoordinator 类的子类。

比如下面这段示例代码中:

@Composable
private fun OnGloballyPositionedDemo() {
    var offsetX by remember { mutableIntStateOf(0) }
    var offsetY by remember { mutableIntStateOf(0) }

    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .onGloballyPositioned {
                    println("全局位置或者大小改变了")
                }
                .offset(offsetX.dp, offsetY.dp)
                .clickable {
                    offsetX = (0..100).random()
                    offsetY = (0..100).random()
                }
                .size(40.dp)
                .background(Color.Green)
        )
    }
}

初始运行时,会触发 onGloballyPositioned() 函数的回调,打印“全局位置或者大小改变了”。然后我们每次点击绿色块,它的全局位置发生改变,也会触发回调,打印“全局位置或者大小改变了”。

回调函数中会提供一个 LayoutCoordinates 类型的参数 coordinates,LayoutCoordinates 是一个接口,而 NodeCoordinator 是实现了这个接口的,实际上 coordinates 参数接收的就是一个 NodeCoordinator 实例,还恰好是 OnGloballyPositionedModifier 所在的 NodeCoordinator 对象。

比如在下面代码中,我们从 onGloballyPositioned() 函数内部拿到的 layoutCoordinates 对象,实际上是 size() 修饰符对应的 NodeCoordiantor 协调器对象。

Box(
    Modifier
    .onGloballyPositioned { layoutCoordinates ->  
        
    }
    .size(100.dp)
)

所以 OnGloballyPositionedModifier 的作用是当它右边的 LayoutModifierNode 所控制的区域或者它所在的 Composable 函数控制的区域,区域的全局位置或者尺寸发生了改变的时候,它的回调函数 onGloballyPositioned() 会被触发。并且参数传入的对象也是区域对应的 NodeCoordinator 对象。

onGloballyPositioned 与 onPlaced 的区别

那 OnGloballyPositionedModifier 和 OnPlacedModifier 有什么区别吗?它们好像都可以获取一块区域尺寸或位置改变后的回调。

Modifier.onGloballyPositioned { layoutCoordinates ->
    // 获取组件在窗口中的位置
    val globalPosition = layoutCoordinates.positionInWindow()
    // 获取组件的尺寸
    val size = layoutCoordinates.size
    Log.d("GlobalPosition", "位置: $globalPosition, 尺寸: $size")
}

Modifier.onPlaced { layoutCoordinates ->
    // 获取组件相对于父组件的位置
    val posInParent = layoutCoordinates.positionInParent()
    Log.d("PlacedPosition", "相对父组件位置: $posInParent")
}

首先它们的回调函数的参数实际传入的对象是相同的,都是其所属的 NodeCoordinator 对象,不同在于回调的时机。

onPlaced() 函数的调用时机是,所在的 NodeCoordiantor 测量完成后,外层 NodeCoordiantor 来摆放时,会被调用,这个时候内层是还没摆放的,你可以做一些事情,来影响到内层 NodeCoordiantor 的摆放。

比如:

var offsetX by remember { mutableIntStateOf(0) }
var offsetY by remember { mutableIntStateOf(0) }

Modifier
    .onPlaced { layoutCoordinates ->
        val posInParent = layoutCoordinates.positionInParent()
        offsetX = posInParent.x.toInt()
        offsetY = posInParent.y.toInt()
    }
    .offset {
        IntOffset(offsetX, offsetY)
    }
    .size(40.dp)

这样界面使用的就不是 (0,0) 偏移了,而是修改后的偏移。

onGloballyPositioned() 函数的调用时机是它所对应的 NodeCoordinator 相对于窗口(window)的位置改变或者尺寸改变时,会被调用。

这个和onPlaced()函数的调用时机是不一样的,比如说在翻聊天记录时,每一条文本相对于界面的位置是会发生改变的,但相对于气泡的位置是不会变的,所以这个过程中,onPlaced()函数不会触发回调,而onGloballyPositioned() 函数会触发回调。

06fcd094-7fd4-4a5d-aed3-179b105e15e4.jpg

那有没有 onPlaced() 函数会触发回调,但 onGloballyPositioned() 函数不会触发回调的情况呢?

理论上存在,比如你看下面这张图,可以形象地展示:

388772e4aa627ae2109c00c8d882881c.gif

“鸡头”相对于“地面”的位置是没有发生改变的,而相对于“自己”是有改变的。

这个只是理论情况下,事实上,只要发生了重新布局 onGloballyPositioned() 函数就会被调用,哪怕全局位置没有改变。

使用原则:因为 onGloballyPositioned() 函数更容易被触发,只要重新布局,就会被调用,所以能用 onPlaced() 函数,就使用onPlaced() 函数,当满足不了需求了,再去使用 onGloballyPositioned() 函数。

写法

写法和使用 onPlaced() 函数是一样的,如下:

Box(
    Modifier
        .onGloballyPositioned { coordinates ->
            // 获取组件在窗口中的位置
            val globalPosition = coordinates.positionInWindow()
            // 获取组件的尺寸
            val size = coordinates.size
            Log.d("GlobalPosition", "位置: $globalPosition, 尺寸: $size")
            
            // 获取相对于父组件的位置
            val positionInParent = coordinates.positionInParent()
            
            // 判断是否在视口中可见
            val isVisible = coordinates.isAttached && coordinates.isVisible()
        }
        .size(100.dp)
        .background(Color.Blue)
)

通过 coordinates 参数,我们可以获取组件的全局位置、尺寸以及其他布局信息,从而实现各种基于位置的交互效果。

原理

OnGloballyPositionedModifier 也是存放在 Modifier.Node 双向链表中,我们在onGloballyPositioned() 函数中写的代码,最终会被 AndroidComposeView 类中的 measureAndLayout() 函数中被调用。

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

override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
    trace("AndroidOwner:measureAndLayout") {
        measureAndLayoutDelegate.measureAndLayout(layoutNode, constraints)
        if (!measureAndLayoutDelegate.hasPendingMeasureOrLayout) {
            measureAndLayoutDelegate.dispatchOnPositionedCallbacks() // ⭐
        }
    }
}

内部实现上,Compose 布局系统在完成测量和布局后,会调用 dispatchOnPositionedCallbacks() 方法,该方法会遍历所有注册的 OnGloballyPositionedModifier,并触发它们的 onGloballyPositioned() 回调。

这个过程是在布局阶段完成后执行的,确保了回调函数接收到的是最终确定的位置和尺寸信息。由于 dispatchOnPositionedCallbacks() 在每次布局变化后都会被调用,这也解释了为什么 onGloballyPositioned() 在每次重新布局时都会被触发。