SwiftUIの GeometryReader に相当する、親のサイズや自身の座標を取得する Jetpack Compose の主な機能は以下の3つです。
用途に合わせて使い分けます。
- BoxWithConstraints(親のサイズに基づいたレイアウト)SwiftUIの GeometryReader に一番近い機能で、画面サイズや親コンテナの制約(最大/最小の幅・高さ)を取得できます。
用途: 画面の向き(縦/横)に応じて表示を変えたり、親のサイズに合わせて子要素のサイズを動的に計算する場合。
取得できる値: Constraints(minWidth, maxWidth, minHeight, maxHeight) - onGloballyPositioned(自身のサイズや絶対座標の取得)コンポーザブルが画面に配置された後、そのサイズ(Size)や画面上の絶対位置(Offset)を取得できる Modifier です。
用途: 他のビューとの相対的な位置関係を計算したり、ドロップダウンメニューの表示位置を計算する場合。
取得できる値: LayoutCoordinates - SubcomposeLayout(サイズに応じた動的な描画)Jetpack Composeにおける最も柔軟で低レベルなレイアウトAPIです。
子要素を測定(Measure)してから、その結果に基づいて配置を決定できます。
用途: 別のコンポーザブルのサイズを測ってから、それに合わせたレイアウトを構築する必要がある場合(自作のカスタムレイアウトなど)。
BoxWithConstraints
Jetpack Composeの BoxWithConstraints は、親レイアウトから与えられた利用可能な画面サイズ(制約)を動的に取得し、そのサイズに応じて表示するUIを切り替える(レスポンシブ対応)ためのコンポーザブルです。 [1, 2]
通常の Box と同様に要素を重ねて配置できるだけでなく、ラムダ式の内部で maxWidth や maxHeight といったサイズ情報に直接アクセスできるのが最大の特徴です。
📋 取得できる主なプロパティ
BoxWithConstraints の中(Scope内)では、以下のプロパティを Dp 単位および Px 単位で取得できます。
maxWidth:利用可能な最大幅(Dp単位)maxHeight:利用可能な最大高さ(Dp単位)minWidth:利用可能な最小幅(Dp単位)minHeight:利用可能な最小高さ(Dp単位)constraints:ピクセル単位の生の制約情報(Constraints型)
💻 基本的な使い方(コード例)
画面の横幅(maxWidth)が 600dp以上か未満か によって、表示するコンポーザブルを Row(横並び)と Column(縦並び)で動的に切り替える実装例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun AdaptiveLayoutExample() { // 1. BoxWithConstraintsを配置 BoxWithConstraints(modifier = Modifier.fillMaxSize()) { // 2. maxWidth(最大幅)を条件分岐に使用 if (maxWidth < 600.dp) { // 画面幅が狭い場合(スマホの縦画面など)は縦並び Column { Text(text = "画面幅が狭いため、縦並びに配置しています") Text(text = "現在の幅: $maxWidth") } } else { // 画面幅が広い場合(タブレットやスマホの横画面など)は横並び Row { Text(text = "画面幅が広いため、横並びに配置しています ") Text(text = "現在の幅: $maxWidth") } } } } |
🛠️ 主なユースケース
- マルチデバイス・レスポンシブ対応:スマホ、タブレット、折りたたみデバイスなど、異なる画面幅に合わせてレイアウトを自動調整したいとき。
- アスペクト比の動的計算:親の幅と高さを比較して、常に最適なアスペクト比で画像やコンテンツを表示したいとき。
- 「もっと見る」ボタンの制御:テキストが親の最大高さを超えるかどうかを判定し、動的に省略してボタンを表示したいとき。
⚠️ パフォーマンスに関する注意点
BoxWithConstraints は非常に便利ですが、多用は避けるべきです。
通常のレイアウト(RowやColumnなど)は、サイズ計測を効率的に1回で行う設計になっています。しかし、BoxWithConstraints はサイズが確定した後に中身のコンポジション(画面の組み立て)を遅延実行するため、通常のコンポーザブルよりも描画のオーバーヘッド(負荷)が大きくなります。
対策:
- 画面のルート付近など、本当にサイズに応じた条件分岐が必要な場所だけに限定して使用してください。
- 単に「親いっぱいに広げたい」だけであれば、
BoxWithConstraintsではなく、Modifier.fillMaxSize()などの Compose修飾子(Modifier) を使用してください。
onGloballyPositioned
Jetpack Composeの Modifier.onGloballyPositioned は、対象のコンポーザブル(UI要素)の画面上の正確な座標やサイズ(幅・高さ)を動的に取得するための修飾子です。
UIの配置やサイズが決定・変更されたタイミングでラムダ式が実行され、引数から詳細な位置情報を取得できます。
💻 基本的な使い方
最も一般的な「コンポーネントのサイズ(幅・高さ)」を取得する基本コードです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.IntSize @Composable fun SizeTrackerExample() { // 取得したサイズを保持するState var componentSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> // 引数の LayoutCoordinates からサイズを取得してStateに代入 componentSize = coordinates.size } ) { Text(text = "幅: ${componentSize.width}, 高さ: ${componentSize.height}") } } |
🎯 主な活用パターンと座標の取得方法
onGloballyPositioned のラムダ式で渡される LayoutCoordinates オブジェクトを使用すると、目的の用途に合わせた様々な位置データを抽出できます。
1. コンポーネントのサイズを取得する
coordinates.size:IntSize型で、ピクセル単位の幅(width)と高さ(height)を取得します。
2. ウィンドウ(画面全体)に対する相対座標を取得する
ツールチップの表示位置の計算や、画面内での表示判定に利用します。
coordinates.localToWindow(Offset.Zero): コンポーネントの左上端の画面座標(Offset型)を計算します。coordinates.boundsInWindow(): 画面全体に対するコンポーネントの矩形領域(Rect型)を丸ごと取得します。
3. 親要素(Root)に対する相対座標を取得する
coordinates.localToRoot(Offset.Zero): アプリのルートレイアウトに対する相対座標を取得します。coordinates.boundsInRoot(): ルートレイアウトに対する矩形領域(Rect型)を取得します。
4. 特定の親コンポーネントからの相対座標を取得する
特定のスクロールコンテナ(Box や Column など)から、その子がどれだけ離れているかを計測します。
parentCoordinates.localChildBoundsOf(coordinates): 親のLayoutCoordinatesを基準にした、子の領域を取得します。
⚠️ 使用時の注意点
- 無限ループ(再描画の頻発)に注意する
onGloballyPositioned内で取得したサイズや座標をもとに、同じ要素のサイズや配置を直接変更するStateを更新すると、配置変更 -> 取得 -> State更新 -> 再描画 -> 配置変更...という無限ループに陥りアプリがクラッシュ・ハングアップする原因になります。 - 初回配置時およびレイアウト変更時のみ動作する
このイベントは、対象のコンポーネントが配置(Layoutフェーズ)されたタイミング、またはスクロールやウィンドウサイズ変更で位置が動いたタイミングでのみ発火します。 - ピクセル単位(Px)で返される
取得できる数値はDpではなくPixel単位です。Composeのスタイル指定(Modifier.width()など)に再利用する場合は、LocalDensity.currentを使ってDpに変換する必要があります。
SubcomposeLayout
SubcomposeLayoutは、ある子要素の計測サイズを基にして、別の子要素を動的にコンポーズ(生成)して配置するための高度なカスタムレイアウト用 API です。
通常の Layout コンポーザブルは「コンポジション(生成) ➡ 計測(Measure) ➡ 配置(Layout)」という固定のフェーズを辿るため、計測したサイズを基に別の子要素を生成することはできません。しかし、SubcomposeLayout を使用すると、計測フェーズの最中に一部のコンポジションを遅延実行することが可能になります。標準コンポーザブルの BoxWithConstraints や LazyColumn、TabRow などの裏側でもこの仕組みが利用されています。
📌 基本的な使い方と実装フロー
SubcomposeLayout 内のラムダでは subcompose 関数を使用します。一意の slotId を指定して子要素をコンポーズし、返ってきた Measurable リストを計測(measure)して配置します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@Composable fun CustomDynamicLayout( modifier: Modifier = Modifier, mainContent: @Composable () -> Unit, dependentContent: @Composable (measuredWidth: Int, measuredHeight: Int) -> Unit ) { SubcomposeLayout(modifier = modifier) { constraints -> // 1. 先にメインとなるコンテンツをサブコンポーズする val mainMeasurables = subcompose("main", mainContent) // 2. メインコンテンツを現在の制約(constraints)で計測する // ※ここでは簡易的に最初の子要素のみを計測 val mainPlaceable = mainMeasurables.first().measure(constraints) // メインの計測結果を取得 val mainWidth = mainPlaceable.width val mainHeight = mainPlaceable.height // 3. メインのサイズを基に、依存するコンテンツを動的にサブコンポーズして計測する val dependentMeasurables = subcompose("dependent") { dependentContent(mainWidth, mainHeight) } val dependentPlaceable = dependentMeasurables.first().measure(constraints) // 4. 親としての最終的なレイアウトサイズを決定し、要素を配置する layout( width = maxOf(mainWidth, dependentPlaceable.width), height = mainHeight + dependentPlaceable.height ) { // 縦に並べて配置する例 mainPlaceable.placeRelative(0, 0) dependentPlaceable.placeRelative(0, mainHeight) } } } |
🛠️ 典型的な活用ユースケース
- 要素の最大サイズに合わせる: リスト内の「最も幅が広いテキスト」を計測し、他のすべての要素の幅をその最大幅に統一する。
- 条件付きコンポーザブルの切り替え: タブの合計幅を計算し、画面幅に収まるなら
TabRow、溢れるならScrollableTabRowに切り替える。 - 動的な仕切り(Divider)の挿入: メイン要素の高さや数に応じて、要素間にぴったり収まる装飾を後から追加する。
⚠️ 使用上の注意点とパフォーマンスへの影響
SubcomposeLayout は非常に強力ですが、以下の理由からどうしても必要な場合を除いて多用すべきではありません。
- 追加のオーバーヘッド: 通常のレイアウトフェーズ中に再度コンポジション(木の生成)を走らせるため、通常の
Layoutよりも処理コストが高くなります。 - 用途の誤りに注意: 「サイズや位置を動的に変えたいだけ」であれば、通常の
LayoutやModifier.layout、あるいはConstraintsの調整だけで実現可能です。サイズに応じて生成する Composable 自体を変化させたい時にのみ使用してください。
もし「単純に画面の最大幅・最大高さを知りたいだけ」であれば、内部で SubcomposeLayout をラップして最適化されている BoxWithConstraints を使う方が安全かつ簡単です。