kako.dev

開発、自作アプリのこと

Composeが座標を返す流れを追ってみる

2023/3/31にDroidKaigi.collect{ #1@Tokyo } でLT登壇しました。

speakerdeck.com

LTの中でComposeの座標取得について触れているんですが、Composeの座標はどのようにセットされてくるのか気になり調べてみました。

Composeの座標を取得する方法

Modifier.onGloballyPositionedで取得できます。LayoutCoordinatesが返ってくるのでそれから座標やサイズを取得できます。

@Composable
fun Sample() {
    Column(
         modifiler = Modifier.onGloballyPositioned {
         
         }
    // ...
}

OnGloballyPositionedModifier

onGloballyPositionedinterface OnGloballyPositionedModifierで定義されています。本体の実装はOnGloballyPositionedModifierImplが定義されていてModifier.onGloballyPositionedOnGloballyPositionedModifierImplを返しています。

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.requireCoordinatorNodeCoordinatorを返すようになっています。

@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

NodeCoordinatorLayoutCoordinatesを継承していました。 onGloballyPositionedNodeCoordinatorを返していると言えそうですが、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周りの理解が数ミリ進みました。