ComposeでのonGloballyPositioned modifier についての調査

:::note info
本記事は ZOZO Advent Calendar 2023 カレンダー Vol.6 の20日目の記事です。
:::
明日は[jon20さん](https://qiita.com/jon20)の記事となります。

いきなりですが、Jetpack ComposeのonGloballyPositionedというmodifierを使用したことはありますでしょうか？

自分は対象となるComposableのサイズ取得時や親Composableに付随した特定の位置に配置したい場合など、何度か使用する機会がありました。ですが、実装のたびに自分が期待する動作をするまで試行錯誤をしてモヤモヤしていたので、今回は基本となる挙動をしっかりと理解することを目的に調査を行っていきたいと思います！

# onGloballyPositionedとは？
onGloballyPositionedはStableなModifierの拡張関数で、window内で要素の位置が変わるたびに呼び出されるとされています。

ただ場合によっては変更された要素の画面に対する相対位置が変更しても、反応しない場合があるので注意してください。[^3]

詳細はDevelopersの[公式ドキュメント](http://qiita.com)[^1]を参考してみてください。

https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier 

## onGloballyPositionedが呼び出されるタイミング
まずはonGloballyPositionedが呼び出されるタイミングについて見てみましょう。

>A modifier whose onGloballyPositioned is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed. Note that it will be called after a composition when the coordinates are finalized.

上記から、onGloballyPositionedが呼び出されるタイミングはCompositionフェーズが終了してかつ座標が確定したタイミングということが分かりました。下図で言うと"If the size or position has changed"の部分となります。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/ad82d8e6-82f4-b747-0b59-db8cd8b15c9a.png"/> <br>引用元[^2]

# 検証条件
今回の調査を行なっていくために使用したプログラムと端末の説明です。
まずはプログラムですが、下記左図の様に赤紫のドラッグ可能な100dpの正方形を検証対象のComposableとして用意します。また、正方形のRootとなるComposableは赤枠、ParentとなるComposableは青枠でそれぞれ囲みます。
検証したいメソッドから取得した数値は、画面下部に表示しています。数値は、赤紫の正方形の位置に伴って更新されます。

検証に使用した端末はPixel2で、端末の詳細情報は下記右図となります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/47ef3102-900e-2d60-2d98-2b2ac471b77f.gif" width=300/>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/44104376-a958-73f6-cbec-fe792d99084e.png" width=300/>

# onGloballyPositionedのpositionIn~()メソッドで取得できる座標の種類

上記の[公式ドキュメント](http://qiita.com)[^1]に記載されているコードのコメントを和訳したのが下記になります。

```kotlin:
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.positionInWindow

Column(
    Modifier.onGloballyPositioned { coordinates ->
        // Columnのサイズ。
        coordinates.size
        // アプリケーションウィンドウに対するColumnの位置。
        coordinates.positionInWindow()
        // Compose rootに対してのColumnの相対的位置.
        coordinates.positionInRoot()
        // These will be the alignment lines provided to the layout (empty here for Column).
        coordinates.providedAlignmentLines
        // This will be a LayoutCoordinates instance corresponding to the parent of Column.
        coordinates.parentLayoutCoordinates
    }
) {
    Box(Modifier.size(20.dp).background(Color.Green))
    Box(Modifier.size(20.dp).background(Color.Blue))
}
```
上記のコメントだとなんとなくイメージがつくと思うのですが、より具体的にどんな数値が取得できるのか検証して下記の図１とテーブルに整理してみました。

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
footer {
  FontColor black
  FontSize 32
}
</style>
scale 500*500
title <img:"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/9759f544-7397-1462-634d-5dde27e8da2d.png">
footer 図１　各positionIn~()メソッドの比較対象となる座標
@enduml
```

|メソッド名|説明|図1で該当する座標の番号|
|---|---|---|
|positionInWindow()|端末ステータスバーの左上の座標1️⃣と赤紫四角の左上の座標差分|1️⃣|
|positionInRoot()|メインスクリーンの左上の座標と赤紫四角の左上の座標差分|2️⃣|
|positionInParent()|自身の親となるComposableの左上の座標と赤紫四角の左上の座標差分|3️⃣ |

例として

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/7154db63-67ff-caa8-42a5-af57a79e0ca5.png", width=300/>


## 気をつけるポイント⚠️
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/03e4b398-6ff2-c9b4-1d85-f11da8eab884.png" width=300/>

positionInRoot()に関してですが、基本的にComposableのみを使用した画面では、Root　Composableのpaddingやoffsetに関係なく、常に画面左上の座標を（０、０）として比較しているという挙動でした。

上図のように、Root　Composableに25dpのpaddingをつけて、赤枠と正方形が交わる時点の座標を観察するとY軸の値が０ではなくpadding分の２５がOffsetとして返されていました。

# onGloballyPositionedのlocal~()メソッドで取得できる座標の種類

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
footer {
  FontColor black
  FontSize 32
}
</style>
scale 500*500
title <img:"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/7bece134-8199-c937-9e49-583049e7b624.png">
footer 図2　各local~()メソッドで取得できる座標の結果
@enduml
```
|メソッド名|説明|
|---|---|
|localToWindow(relativeToLocal: Offset)|端末ステータスバーの左上の座標と引数で渡す座標relativeToLocalの差分|
|localToRoot(relativeToLocal: Offset)|メインスクリーンの左上の座標と引数で渡す座標relativeToLocalの差分|

上記テーブルのメソッド検証時に用いたコードが下記になります。
各local~()メソッドは、赤紫正方形の中心座標からRoot,Parentそれぞれの左上の座標までの差分を取得するためのメソッドとなっており、結果は図２のようになります。

赤紫正方形のサイズが100dpのため、図１の結果と比較してXとYの座標数値がそれぞれ約50dp増えているのが分かると思います。

```kotlin:
positionInLocalToWindow = localToWindow(
                    //赤紫正方形の中心座標
                    Offset(
                            coordinates.size.width.toFloat() * 0.5F,
                            coordinates.size.height.toFloat() * 0.5F,
                        )
                    )

positionInLocalToRoot = localToRoot(
                    //赤紫正方形の中心座標
                    Offset(
                        coordinates.size.width.toFloat() * 0.5F,
                        coordinates.size.height.toFloat() * 0.5F,
                        )
                    )
```

# まとめ
今回はonGloballyPositionedというmodifierの基本メソッドの調査を行いましたが、取得されるOffsetの意味や挙動について理解を深められたと思います。

画面外のリストのアイテムに対しても同様な挙動になるのかも興味を持ったので、新しい発見があればまたの機会に投稿できればと思います。

また、少しでもこの記事が皆さんの役に立つ機会があれば嬉しいです。

# 使用したコード
コード → https://gist.github.com/masah517/d5e10123020f0800634387efe2b17336

[^1]: https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier

[^2]: https://developer.android.com/jetpack/compose/phases

[^3]: https://kako351.dev/entry/2023/04/02/Compose%E3%81%8C%E5%BA%A7%E6%A8%99%E3%82%92%E8%BF%94%E3%81%99%E6%B5%81%E3%82%8C%E3%82%92%E8%BF%BD%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B