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(). 이 네 값을 계산한 후 child.layout(childLeft, childTop, childRight, childBottom)이 호출됩니다.
FlowLayout을 만들어 보겠습니다. 이것은 자식 View를 행으로 배열하고 현재 행이 가득 차면 요소를 새 행으로 줄 바꿈하는 커스텀 ViewGroup입니다. 한 평면에서 Flexbox의 wrap에 해당합니다. 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 수명 주기의 두 순차적 단계로, 근본적으로 다른 작업을 수행합니다. 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()에 접근하면 이전 주기의 값이나 0이 반환됩니다. 따라서 onMeasure에서 크기를 계산하려면 MeasureSpec과 자식을 순차적으로 사용해야 합니다.
패딩을 고려하지 않은 위치 지정 — onLayout을 구현할 때 첫 번째 실수입니다. 개발자는 자식 View의 초기 좌표에 부모의 paddingLeft와 paddingTop을 추가하는 것을 자주 잊습니다. 결과적으로 자식은 setPadding() 또는 XML 마크업을 통해 설정된 패딩을 무시하고 ViewGroup 가장자리에 표시됩니다. 올바른 계산: childLeft = paddingLeft + offsetX.
보이지 않는 자식을 위한 레이아웃 호출 — 두 번째 일반적인 문제입니다. ViewGroup에 가시성이 GONE인 자식 View가 포함된 경우 위치를 지정할 필요가 없습니다. 공간을 차지하지 않습니다. 그러나 onLayout은 GONE 자식을 건너뛰어 이 경우를 올바르게 처리해야 합니다. INVISIBLE 자식의 경우 레이아웃을 계속 호출해야 합니다. 표시되지 않더라도 공간을 유지합니다.
changed 매개변수 무시 — 세 번째 실수입니다. 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 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.