안드로이드 프레임워크는 Activity 가 포커스를 가질 때 레이아웃을 그리도록 요청함, 안드로이드 프레임워크는 Activity 가 레이아웃 계층 구조의 루트 노드를 제공해야 그리는 과정을 처리할 수 있음
안드로이드 프레임워크는 레이아웃의 루트 노드를 그리고 레이아웃 트리를 측정하고 그림, 트리를 따라 그리며 유효하지 않은 영역(invalid region)과 교차하는 뷰를 렌더링함, 각 뷰 그룹은 draw() 메소드를 사용해 각 하위 요소들을 그리도록 요청해야 하며 각 뷰는 자신을 그려야 함, 트리는 전위 순회(pre-order traversal)되기 때문에 프레임워크는 부모 노드를 먼저 그리고 자식 노드를 그림(부모가 뒤에 그려짐), 그리고 형제 뷰는 트리에 등장하는 순서대로 그림
노트: 프레임워크는 유효한 영역(valid region이라 써있지만 여기서는 그려야 하는 영역을 뜻함)에 있지 않은 뷰 객체를 그리지 않습니다. 또한 뷰의 배경을 그리는 것도 프레임워크가 합니다.
invalidate()를 호출해 뷰를 강제로 그릴 수 있습니다.
measure(int, int) 를 수행하고 뷰 트리를 하향식 순회(top-down traversal)함, 각 뷰는 트리를 순회하는 동안 필요한 공간을 하위로 보냄, 측정 단계의 끝에는 모든 뷰가 자신의 측정치를 가지고 있게 됨, 그러면 프레임워크는 layout(int, int, int, int) 를 수행해 두 번째 단계인 레이아웃 단계를 하향식 순회로 진행함, 이 단계에서 각 부모는 모든 자식들을 측정 단계에서 측정된 크기를 사용해 위치를 잡아야 함뷰 객체의 measure() 메소드가 반환될 때 뷰 객체의 모든 자손들의 것을 포함하는 getMeasuredWidth() 와 getMeasuredHeight() 의 값이 설정됨, 뷰 객체의 측정된 너비와 측정된 높이 값은 반드시 뷰 객체의 부모에 의해 설정된 제약을 준수해야 함, 이는 측정 단계가 끝날 때 모든 부모가 모든 자식들의 측정치를 수용할 수 있도록 보장함
부모 뷰는 measure() 를 자식에 대해 한번 이상 호출할 수 있음, 예를 들어 부모가 자식을 한번 지정되지 않은 크기로 측정해 자식이 선호하는 크기를 결정할 수 있음, 그 후에 자식의 제약되지 않은 크기의 합이 너무 크거나 너무 작으면 부모는 measure() 를 다시 호출해 제약된 자식의 크기를 가져옴
측정 단계에서 크기 정보를 교환하기 위해 두 클래스를 사용함, ViewGroup.LayoutParams 클래스는 View 객체가 선호하는 크기와 위치를 전달하는 데 사용하는 클래스임, 기본 ViewGroup.LayoutParams 클래스는 View 의 선호하는 너비와 높이를 설명함, 각 크기를 다음 중 하나로 설명함
MATCH_PARENT : View 가 선호하는 크기가 부모의 크기에서 패딩을 뺀 크기라는 뜻WRAP_CONTENT : View 가 선호하는 크기가 내용의 크기를 감쌀만큼의 크기에 패딩을 더한 크기라는 뜻ViewGroup 의 다양한 서브클래스를 위한 ViewGroup.LayoutParams 의 서브클래스가 있음, 예시로 RelativeLayout 은 자식 View 객체를 수직과 수평으로 가운데 정렬을 하는 능력을 가지는 ViewGroup.LayoutParams 의 서브클래스를 가지고 있음
MeasureSpec 객체는 부모에서 자식으로 요구사항을 전달하기 위해 사용됨, MeasureSpec 은 다음 세 가지 모드 중 하나를 가짐
UNSPECIFIED : 부모가 자식 View 의 목표 크기를 결정하기 위해 사용함, 예시로 LinearLayout 은 자식 View 의 높이를 UNSPECIFIED 로 설정하고 너비를 EXACTLY 240으로 설정해 measure() 를 호출해 자식 View 가 너비를 240 픽셀로 설정할 때 어떤 높이를 가지려고 하는지 알 수 있음EXACTLY : 부모가 자식에 정확한 크기를 적용할 때 사용함, 자식은 반드시 이 크기를 사용하고 자식의 모든 자손들이 이 크기안에 맞도록 보장해야 함AT MOST : 부모가 자식에 최대 크기를 적용할 때 사용함, 자식은 본인과 모든 자손들이 이 크기안에 맞도록 보장해야 함requestLayout() 을 호출해야 함, 이 메소드는 일반적으로 경계에 더이상 맞지 않다고 생각되면 View 자체적으로 호출함사용자 지정 측정 및 레이아웃 로직을 구현하려면 onMeasure(int, int) 와 onLayout(boolean, int, int, int, int) 메소드를 오버라이드해야 함, 이 메소드들은 각각 measure(int, int) 와 layout(int, int, int, int) 에 의해 호출됨, measure(int, int) 와 layout(int, int, int, int) 메소드는 final 이기 때문에 오버라이드될 수 없음
다음 예제는 WindowManager 샘플 앱에서 SplitLayout 클래스가 이를 어떻게 하는지 보여주는 예제임, SplitLayout 이 두 개 이상의 자식 뷰를 가지고 있고 디스플레이가 접힌다면 두 자식 뷰를 접히는 양쪽 면에 위치하게 함, 다음 예제는 측정과 레이아웃을 오버라이딩하는 것을 보여주지만 이 동작을 하는 것은 생산성을 위해 SlidingPaneLayout 을 사용해야 함
/**
* 윈도우 전체를 가로지르는 디스플레이 특징에 따라 분할된 두 개의 뷰를 위한
* 분할 레이아웃 예시입니다. 시작 뷰와 종료 뷰가 모두 추가되면,
* 접힘(fold) 또는 경첩(hinge) 같은 디스플레이 특징이 영역을 둘로 나누는지 확인하고,
* 두 뷰를 나란히 혹은 위아래로 배치합니다.
*/
class SplitLayout : FrameLayout {
private var windowLayoutInfo: WindowLayoutInfo? = null
private var startViewId = 0
private var endViewId = 0
private var lastWidthMeasureSpec: Int = 0
private var lastHeightMeasureSpec: Int = 0
...
fun updateWindowLayout(windowLayoutInfo: WindowLayoutInfo) {
this.windowLayoutInfo = windowLayoutInfo
requestLayout()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val startView = findStartView()
val endView = findEndView()
val splitPositions = splitViewPositions(startView, endView)
if (startView != null && endView != null && splitPositions != null) {
val startPosition = splitPositions[0]
val startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(), EXACTLY)
val startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(), EXACTLY)
startView.measure(startWidthSpec, startHeightSpec)
startView.layout(
startPosition.left, startPosition.top, startPosition.right,
startPosition.bottom
)
val endPosition = splitPositions[1]
val endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), EXACTLY)
val endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(), EXACTLY)
endView.measure(endWidthSpec, endHeightSpec)
endView.layout(
endPosition.left, endPosition.top, endPosition.right,
endPosition.bottom
)
} else {
super.onLayout(changed, left, top, right, bottom)
}
}
/**
* 이 뷰에 대한 분할 위치를 가져옵니다.
* 분할을 정의하는 사각형(Rect)을 반환하거나 또는 분할이 없으면 null을 반환합니다.
*/
private fun splitViewPositions(startView: View?, endView: View?): Array? {
if (windowLayoutInfo == null || startView == null || endView == null) {
return null
}
// 패딩을 포함하는 뷰의 영역을 계산합니다.
val paddedWidth = width - paddingLeft - paddingRight
val paddedHeight = height - paddingTop - paddingBottom
windowLayoutInfo?.displayFeatures
?.firstOrNull { feature -> isValidFoldFeature(feature) }
?.let { feature ->
getFeaturePositionInViewRect(feature, this)?.let {
if (feature.bounds.left == 0) { // 수평 레이아웃.
val topRect = Rect(
paddingLeft, paddingTop,
paddingLeft + paddedWidth, it.top
)
val bottomRect = Rect(
paddingLeft, it.bottom,
paddingLeft + paddedWidth, paddingTop + paddedHeight
)
if (measureAndCheckMinSize(topRect, startView) &&
measureAndCheckMinSize(bottomRect, endView)
) {
return arrayOf(topRect, bottomRect)
}
} else if (feature.bounds.top == 0) { // 수직 레이아웃.
val leftRect = Rect(
paddingLeft, paddingTop,
it.left, paddingTop + paddedHeight
)
val rightRect = Rect(
it.right, paddingTop,
paddingLeft + paddedWidth, paddingTop + paddedHeight
)
if (measureAndCheckMinSize(leftRect, startView) &&
measureAndCheckMinSize(rightRect, endView)
) {
return arrayOf(leftRect, rightRect)
}
}
}
}
// 이전에 이미 자식을 측정하고 맞추려고 시도했습니다.
// 이 값이 맞지 않다면 다시 측정하고 값을 저장합니다.
measure(lastWidthMeasureSpec, lastHeightMeasureSpec)
return null
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
lastWidthMeasureSpec = widthMeasureSpec
lastHeightMeasureSpec = heightMeasureSpec
}
/**
* 자식 뷰를 측정하고 이 값이 제공된 사각형에 맞는지 확인합니다.
* 이 메소드는 자식 뷰의 측정된 너비와 높이를 위해 저장된 값을 업데이트 하는
* [View.measure]를 호출합니다. 만약 뷰가 다른 크기 값을 가지면
* 다시 측정합니다.
*/
private fun measureAndCheckMinSize(rect: Rect, childView: View): Boolean {
val widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), AT_MOST)
val heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), AT_MOST)
childView.measure(widthSpec, heightSpec)
return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 &&
childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0
}
private fun isValidFoldFeature(displayFeature: DisplayFeature) =
(displayFeature as? FoldingFeature)?.let { feature ->
getFeaturePositionInViewRect(feature, this) != null
} ?: false
}
원문: https://developer.android.com/guide/topics/ui/how-android-draws