作用
来到 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()
函数会触发回调。
那有没有 onPlaced()
函数会触发回调,但 onGloballyPositioned()
函数不会触发回调的情况呢?
理论上存在,比如你看下面这张图,可以形象地展示:
“鸡头”相对于“地面”的位置是没有发生改变的,而相对于“自己”是有改变的。
这个只是理论情况下,事实上,只要发生了重新布局 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()
在每次重新布局时都会被触发。