2023/3/31にDroidKaigi.collect{ #1@Tokyo } でLT登壇しました。
LTの中でComposeの座標取得について触れているんですが、Composeの座標はどのようにセットされてくるのか気になり調べてみました。
Composeの座標を取得する方法
Modifier.onGloballyPositioned
で取得できます。LayoutCoordinates
が返ってくるのでそれから座標やサイズを取得できます。
@Composable fun Sample() { Column( modifiler = Modifier.onGloballyPositioned { } // ... }
OnGloballyPositionedModifier
onGloballyPositioned
はinterface OnGloballyPositionedModifier
で定義されています。本体の実装はOnGloballyPositionedModifierImpl
が定義されていてModifier.onGloballyPositioned
はOnGloballyPositionedModifierImpl
を返しています。
private class OnGloballyPositionedModifierImpl( val callback: (LayoutCoordinates) -> Unit, inspectorInfo: InspectorInfo.() -> Unit ) : OnGloballyPositionedModifier, InspectorValueInfo(inspectorInfo) { override fun onGloballyPositioned(coordinates: LayoutCoordinates) { callback(coordinates) } // ... } @Stable fun Modifier.onGloballyPositioned( onGloballyPositioned: (LayoutCoordinates) -> Unit ) = this.then( OnGloballyPositionedModifierImpl( callback = onGloballyPositioned, inspectorInfo = debugInspectorInfo { name = "onGloballyPositioned" properties["onGloballyPositioned"] = onGloballyPositioned } ) )
LayoutNode
LayoutNode
はComposeのNode要素を表すクラスです。このクラス内にはdispatchOnPositionedCallbacks
があり、その中でonGloballyPositioned
を呼び出しています。
何やらNodes.GlobalPositionAware
を扱っていそうです。
@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)) } }
Nodes.GlobalPositionAware
Nodes.GlobalPositionAware
の型はNodeKind
になっているようです。
it.requireCoordinator
はNodeCoordinator
を返すようになっています。
@OptIn(ExperimentalComposeUiApi::class) internal object Nodes { // ... val GlobalPositionAware = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8) // ... } @JvmInline internal value class NodeKind<T>(val mask: Int)
@ExperimentalComposeUiApi internal fun DelegatableNode.requireCoordinator(kind: NodeKind<*>): NodeCoordinator { val coordinator = node.coordinator!! return if (coordinator.tail !== this) coordinator else if (kind.includeSelfInTraversal) coordinator.wrapped!! else coordinator }
NodeCoordinator
NodeCoordinator
はLayoutCoordinates
を継承していました。
onGloballyPositioned
はNodeCoordinator
を返していると言えそうですが、callbackの型でLayoutCoordinates
になっているのでLayoutCoordinates
で定義されているものを呼び出せます。
internal abstract class NodeCoordinator( override val layoutNode: LayoutNode, ) : LayoutCoordinates, //...
localToRoot
LayoutCoordinates
で定義されたlocalToRoot
は、NodeCoordinator
で以下のようになっています。
Offset
を返すようになってます。
override fun localToRoot(relativeToLocal: Offset): Offset { check(isAttached) { ExpectAttachedLayoutCoordinates } var coordinator: NodeCoordinator? = this var position = relativeToLocal while (coordinator != null) { position = coordinator.toParentPosition(position) coordinator = coordinator.wrappedBy } return position } open fun toParentPosition(position: Offset): Offset { val layer = layer val targetPosition = layer?.mapOffset(position, inverse = false) ?: position return targetPosition + this.position }
最後
ざっくりとした追い方ですが、LayoutNode
周りの理解が数ミリ進みました。