onLayout() はViewGroupクラスのメソッドで、親コンテナの座標平面上で子Viewの位置とサイズを決定します。Androidシステムは測定フェーズ(onMeasure)の後、各子Viewの測定された幅と高さがすでにわかった時点でonLayoutを呼び出します。Android Developers Documentation(2026)によると、標準のViewGroup実装は子の自動位置決めを行わないため、onLayoutはカスタムViewGroupでオーバーライドする必須メソッドです。
重要ポイント
onLayout(boolean changed, int l, int t, int r, int b) はViewGroupクラスのprotectedメソッドで、親コンテナ内に子Viewを配置するためにシステムによって呼び出されます。開発者は、カスケード、グリッド、千鳥配置、任意の座標など、標準的でない要素配置のカスタムViewGroupを作成するときにこのメソッドをオーバーライドします。各子Viewはchild.layout()の呼び出しを通じて最終的な境界を受け取ります。
changedパラメータは、最後のレイアウト以降にViewGroup自体の位置またはサイズが変更されたかどうかを示します。changedがtrueの場合、すべての子要素もおそらく再配置が必要です。パラメータl、t、r、bは、親の座標系におけるViewGroupの左上隅と右下隅の座標です。onLayout内で、開発者はこれらの値を子の配置のための開始座標として使用します。
ViewGroupはonLayoutをオーバーライドする唯一のクラスです。通常のView(ViewGroupではない)には子要素がなく、onLayoutを必要としません。その位置決めは親コンテナによって処理されます。通常のViewがonLayoutをオーバーライドしても、システムはそれを呼び出しません。これは、任意のViewに対して呼び出されるonMeasureとの基本的な違いです。
レイアウトフェーズは、ルートView上のパブリックメソッドlayout(int l, int t, int r, int b)の呼び出しから始まります。このメソッドはView自体の最終座標を設定し、ViewがViewGroupの場合はonLayoutを呼び出します。次にonLayoutは各子要素に対してchild.layout()を再帰的に呼び出し、このプロセスが階層を下って繰り返されます。このようにして、レイアウトはルートからリーフへと伝播します。
onLayoutを呼び出す前に、システムは前のサイクルと比較してViewの寸法が変更されたかどうかを確認します。寸法が変更されておらず、requestLayoutが呼び出されていない場合、onLayoutは呼び出されないことがあります。システムは前のレイアウトの結果を使用します。これは、寸法ではなく内容のみが変更されるアニメーションやスクロール中に、不要な位置の再計算を防ぐ最適化です。
requestLayout()は、Viewのレイアウトが古くなり再計算が必要であることをシステムに通知するViewメソッドです。requestLayoutを呼び出すと完全なサイクルがトリガーされます:最初にonMeasureが呼び出され、次にonLayout、次にonDrawが呼び出されます。再描画のみをトリガーするinvalidateとは異なり、requestLayoutは寸法と位置の完全な再計算をトリガーします。requestLayoutの過剰な呼び出しは、パフォーマンス問題の一般的な原因です。
l(left) — 親の座標系におけるViewGroupの左端のX座標。t(top) — 上端のY座標。r(right) — 右端のX座標。b(bottom) — 下端のY座標。ViewGroupの幅はr - l、高さはb - tとして計算されます。これらの座標には、ViewGroup自体のすべてのパディングがすでに含まれています。
onLayout内で、開発者は各子Viewに対してchild.layout(int childLeft, int childTop, int childRight, int childBottom)を呼び出します。child.layoutに渡される座標は、親ViewGroupの座標系でなければなりません。通常、childLeftとchildTopは親のパディングを考慮して計算されます:childLeft = l + paddingLeft + offsetX、childTop = t + paddingTop + offsetY。
| パラメータ | 説明 | 一般的な使用法 |
|---|---|---|
| l(left) | 親内のViewGroupの左端の座標 | 子要素のX軸の開始点 |
| t(top) | 親内のViewGroupの上端の座標 | 子要素のY軸の開始点 |
| r(right) | 親内のViewGroupの右端の座標 | 幅の上限、r - l = getWidth() |
| b(bottom) | 親内のViewGroupの下端の座標 | 高さの上限、b - t = getHeight() |
子座標は次の式で計算されます:childLeft = l + paddingLeft +(marginLeftがある場合)、childRight = childLeft + child.getMeasuredWidth()。垂直軸についても同様です:childTop = t + paddingTop +(marginTop)、childBottom = childTop + child.getMeasuredHeight()。これら4つの値を計算した後、child.layout(childLeft, childTop, childRight, childBottom)が呼び出されます。
FlowLayoutを作成しましょう。これは、子Viewを行に配置し、現在の行がいっぱいになると要素を新しい行に折り返すカスタムViewGroupです。これは1平面でのFlexboxのラップ相当品です。onLayoutはすべての子Viewを反復処理し、それぞれの位置を計算して、正しい境界でchild.layout()を呼び出します。
class FlowLayout(context: Context)
: ViewGroup(context) {
private val horizontalSpacing = 12
private val verticalSpacing = 8
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val parentWidth =
MeasureSpec.getSize(widthMeasureSpec)
var rowX = paddingLeft
var rowY = paddingTop
var maxRowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child,
widthMeasureSpec, 0,
heightMeasureSpec, 0)
if (rowX + child.measuredWidth >
parentWidth - paddingRight) {
rowX = paddingLeft
rowY += maxRowHeight + verticalSpacing
maxRowHeight = 0
}
rowX += child.measuredWidth +
horizontalSpacing
maxRowHeight = maxOf(maxRowHeight,
child.measuredHeight)
}
val totalHeight = rowY + maxRowHeight +
paddingBottom
setMeasuredDimension(
resolveSize(parentWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec))
}
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
val parentWidth = r - l
var rowX = paddingLeft
var rowY = paddingTop
var maxRowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val cw = child.measuredWidth
val ch = child.measuredHeight
if (rowX + cw >
parentWidth - paddingRight) {
rowX = paddingLeft
rowY += maxRowHeight + verticalSpacing
maxRowHeight = 0
}
child.layout(rowX, rowY,
rowX + cw, rowY + ch)
rowX += cw + horizontalSpacing
maxRowHeight =
maxOf(maxRowHeight, ch)
}
}
override fun generateLayoutParams(attrs: AttributeSet?)
: LayoutParams =
MarginLayoutParams(context, attrs)
}
onMeasureとonLayoutは、Viewライフサイクルの2つの連続したフェーズで、根本的に異なるタスクを実行します。onMeasureはViewの望ましい(測定された)寸法を決定し、onLayoutは実際の(最終的な)座標と寸法を設定します。主な違い:onMeasureでは寸法は中間的であり、後で親によって調整される可能性がありますが、onLayoutでは各子Viewの最終位置が固定されます。
onMeasureは、リーフView(TextView、ImageView、Button)を含むすべてのViewに対して呼び出されます。onLayoutはViewGroupに対してのみ呼び出されます。これは、位置決めがView自体ではなく親コンテナの責任であるためです。リーフViewは、親のonLayoutから呼び出されたlayout()を通じて位置を受け取ります。
getMeasuredWidth()とgetMeasuredHeight()はonMeasureの後に利用可能になりますが、getWidth()とgetHeight()はonLayoutの後にのみ利用可能になります。onMeasure内でgetWidth()にアクセスすると、前のサイクルの値またはゼロが返されます。したがって、onMeasureで寸法を計算するには、MeasureSpecと子を順次使用する必要があります。
パディングを考慮しない位置決め — onLayoutを実装するときの最初の間違い。開発者は子Viewの初期座標に親のpaddingLeftとpaddingTopを追加するのを忘れることがよくあります。その結果、子はsetPadding()またはXMLマークアップで設定されたパディングを無視して、ViewGroupの端に表示されます。正しい計算:childLeft = paddingLeft + offsetX。
非表示の子に対するレイアウト呼び出し — 2番目の一般的な問題。ViewGroupに可視性がGONEの子Viewが含まれている場合、それらを配置する必要はありません。スペースを占有しません。ただし、onLayoutはこのケースを正しく処理し、GONEの子をスキップする必要があります。INVISIBLEの子については、レイアウトを呼び出す必要があります。表示されていなくてもスペースを保持します。
changedパラメータの無視 — 3番目の間違い。changedパラメータは、ViewGroupの寸法または位置が変更されたかどうかを示します。changed == falseの場合、すべての子要素のレイアウトを再計算せずにキャッシュされた座標を使用できます。ただし、完全なレイアウトキャッシュは複雑なタスクであり、ほとんどの実装ではonLayoutは毎回単純にすべての要素を再計算します。これは子の数が少ない場合に許容されます。
よくある質問
はい、可能です。ViewGroupが標準のLayoutParamsを使用し、カスタム位置決めロジックを追加しない場合。ただし、ViewGroupの標準のonLayout実装は何もアクションを実行しません。子要素は配置されません。実際には、すべてのViewGroup(LinearLayout、RelativeLayout、FrameLayout)がonLayoutをオーバーライドします。
layout()はViewのpublic finalメソッドで、システムまたは親ViewGroupによって呼び出されます。View自体の座標を設定し、ViewがViewGroupの場合はonLayoutを呼び出します。onLayout()はprotectedメソッドで、開発者が子要素のカスタム配置のためにオーバーライドします。
技術的には — はい、呼び出せます。しかし、これは強く推奨されません。無限再帰(requestLayout → onMeasure → onLayout → requestLayout)につながるためです。onLayout内でrequestLayoutが呼び出されると、システムはStackOverflowError例外をスローします。すべての寸法変更はonLayoutの前に実行される必要があります。
レイアウトアニメーション(LayoutTransition)は子Viewの位置の変更をインターセプトし、トランジションアニメーションを適用します。LayoutTransitionが有効な場合、onLayoutは最初に最終位置を設定し、次にLayoutTransitionが古い位置から新しい位置への移動をアニメーション化します。これには、適切な最終座標を使用した正しいonLayout実装が必要です。
invalidate()はdrawフェーズ(再描画)のみをトリガーし、measureとlayoutには影響しません。onLayoutをトリガーするには、requestLayout()を呼び出す必要があります。これにより完全なサイクル(measure → layout → draw)が開始されます。invalidateは、寸法と位置が変更されない場合に外観を更新するためにより効率的です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。