Android 프레임워크는 Activity
가 포커스를 받을 때 레이아웃을 그리도록 Activity
에 요청합니다. Android 프레임워크는 그리기 절차를 처리하지만 Activity
에서는 레이아웃 계층 구조의 루트 노드를 제공해야 합니다.
Android 프레임워크는 레이아웃의 루트 노드를 그리고 레이아웃 트리를 측정하고 그립니다. 트리를 이동하고, 잘못된 영역을 교차하는 각 View
를 렌더링하여 그립니다.
각 ViewGroup
은 draw()
메서드를 사용하여 각 하위 요소를 그리도록 요청하고 각 View
는 자체 그리기를 담당합니다. 트리는 사전 순서로 순회하므로 프레임워크는 하위 요소 전에(즉, 하위 요소 뒤에) 상위 요소를 그리고 트리에서 표시되는 순서로 동위 요소를 그립니다.
Android 프레임워크는 측정 패스와 레이아웃 패스라는 두 패스 프로세스로 레이아웃을 그립니다. 프레임워크는 measure(int, int)
에서 측정 패스를 실행하고 View
트리의 하향식 순회를 실행합니다. 각 View
는 반복 중에 트리 아래로 크기 사양을 푸시합니다. 측정 패스가 끝날 때 모든 View
는 측정값을 저장합니다. 프레임워크는 layout(int, int, int, int)
에서 두 번째 패스를 실행하며 또한 하향식입니다. 이 패스 동안 각 상위 요소는 측정 패스에서 계산된 크기를 사용하여 모든 하위 요소를 배치합니다.
레이아웃 프로세스의 두 패스는 다음 섹션에서 더 자세히 설명합니다.
측정 패스 시작
View
객체의 measure()
메서드가 반환되면 getMeasuredWidth()
및 getMeasuredHeight()
값과 함께 모든 View
객체의 하위 요소 값을 설정합니다. View
객체의 측정된 너비 및 측정된 높이 값은 View
객체의 상위 요소가 부여한 제약 조건을 준수합니다. 그러면 측정 패스가 종료되고 모든 상위 요소에서 하위 요소의 측정값을 모두 수락합니다.
상위 요소 View
에서는 하위 요소를 대상으로 두 번 이상 measure()
를 호출할 수 있습니다. 예를 들어 상위 요소는 지정되지 않은 크기로 하위 요소를 한 번 측정하여 원하는 크기를 결정할 수 있습니다. 하위 요소의 제한되지 않은 크기의 합계가 너무 크거나 너무 작으면 상위 요소는 하위 요소의 크기를 제한하는 값으로 measure()
를 다시 호출할 수 있습니다.
측정 패스에서는 두 개의 클래스를 사용하여 크기를 전달합니다. ViewGroup.LayoutParams
클래스는 View
객체가 선호하는 크기 및 위치를 전달하는 방법입니다. 기본 ViewGroup.LayoutParams
클래스는 View
의 기본 너비 및 높이를 설명합니다. 각 크기의 경우 다음 중 하나를 지정할 수 있습니다.
- 정확한 크기
MATCH_PARENT
:View
의 기본 크기가 상위 요소의 크기에서 패딩을 뺀 값입니다.WRAP_CONTENT
:View
의 기본 크기가 콘텐츠와 패딩을 포함할 만큼만 큽니다.
ViewGroup
의 여러 다른 서브클래스에 맞는 ViewGroup.LayoutParams
의 서브클래스가 있습니다. 예를 들어 RelativeLayout
에는 ViewGroup.LayoutParams
의 서브클래스가 있습니다. 이 클래스에는 하위 요소 View
객체를 가로 및 세로 방향으로 중앙에 두는 기능이 포함됩니다.
MeasureSpec
객체는 상위 요소에서 하위 요소로 요구사항을 트리에서 아래로 푸시하는 데 사용합니다. MeasureSpec
은 다음 세 가지 모드 중 하나일 수 있습니다.
UNSPECIFIED
: 상위 요소가 이를 사용하여 하위View
의 타겟 크기를 결정합니다. 예를 들어LinearLayout
은 높이가UNSPECIFIED
로 설정되고 너비가EXACTLY
240으로 설정된 하위 요소에서measure()
를 호출하여 너비가 240픽셀일 경우 하위 요소View
의 높이를 알아낼 수 있습니다.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)
메서드를 재정의하려고 하지 마세요. 두 메서드는 모두 final
이므로 재정의할 수 없습니다.
다음 예는 WindowManager 샘플 앱의 `SplitLayout` 클래스에서 이 작업을 실행하는 방법을 보여줍니다. SplitLayout
에 두 개 이상의 하위 뷰가 있고 디스플레이에 접히는 부분이 있는 경우 두 개의 하위 뷰를 접히는 부분의 양쪽에 배치합니다. 다음 예는 측정 및 레이아웃을 재정의하는 사용 사례를 보여주지만 프로덕션의 경우 이 동작을 원한다면 SlidingPaneLayout
을 사용하세요.
Kotlin
/** * An example of split-layout for two views, separated by a display * feature that goes across the window. When both start and end views are * added, it checks whether there are display features that separate the area * in two—such as a fold or hinge—and places them side-by-side or * top-bottom. */ 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) } } /** * Gets the position of the split for this view. * @return A rect that defines of split, or {@code null} if there is no split. */ private fun splitViewPositions(startView: View?, endView: View?): Array? { if (windowLayoutInfo == null || startView == null || endView == null) { return null } // Calculate the area for view's content with padding. 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) { // Horizontal layout. 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) { // Vertical layout. 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) } } } } // You previously tried to fit the children and measure them. Since they // don't fit, measure again to update the stored values. measure(lastWidthMeasureSpec, lastHeightMeasureSpec) return null } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) lastWidthMeasureSpec = widthMeasureSpec lastHeightMeasureSpec = heightMeasureSpec } /** * Measures a child view and sees if it fits in the provided rect. * This method calls [View.measure] on the child view, which updates its * stored values for measured width and height. If the view ends up with * different values, measure again. */ 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 }
Java
/** * An example of split-layout for two views, separated by a display feature * that goes across the window. When both start and end views are added, it checks * whether there are display features that separate the area in two—such as * fold or hinge—and places them side-by-side or top-bottom. */ public class SplitLayout extends FrameLayout { @Nullable private WindowLayoutInfo windowLayoutInfo = null; private int startViewId = 0; private int endViewId = 0; private int lastWidthMeasureSpec = 0; private int lastHeightMeasureSpec = 0; ... void updateWindowLayout(WindowLayoutInfo windowLayoutInfo) { this.windowLayoutInfo = windowLayoutInfo; requestLayout(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { @Nullable View startView = findStartView(); @Nullable View endView = findEndView(); @Nullable ListsplitPositions = splitViewPositions(startView, endView); if (startView != null && endView != null && splitPositions != null) { Rect startPosition = splitPositions.get(0); int startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(), EXACTLY); int startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(), EXACTLY); startView.measure(startWidthSpec, startHeightSpec); startView.layout( startPosition.left, startPosition.top, startPosition.right, startPosition.bottom ); Rect endPosition = splitPositions.get(1); int endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), EXACTLY); int endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(), EXACTLY); startView.measure(endWidthSpec, endHeightSpec); startView.layout( endPosition.left, endPosition.top, endPosition.right, endPosition.bottom ); } else { super.onLayout(changed, left, top, right, bottom); } } /** * Gets the position of the split for this view. * @return A rect that defines of split, or {@code null} if there is no split. */ @Nullable private List splitViewPositions(@Nullable View startView, @Nullable View endView) { if (windowLayoutInfo == null || startView == null || endView == null) { return null; } int paddedWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int paddedHeight = getHeight() - getPaddingTop() - getPaddingBottom(); List displayFeatures = windowLayoutInfo.getDisplayFeatures(); @Nullable DisplayFeature feature = displayFeatures .stream() .filter(item -> isValidFoldFeature(item) ) .findFirst() .orElse(null); if (feature != null) { Rect position = SampleToolsKt.getFeaturePositionInViewRect(feature, this, true); Rect featureBounds = feature.getBounds(); if (featureBounds.left == 0) { // Horizontal layout. Rect topRect = new Rect( getPaddingLeft(), getPaddingTop(), getPaddingLeft() + paddedWidth, position.top ); Rect bottomRect = new Rect( getPaddingLeft(), position.bottom, getPaddingLeft() + paddedWidth, getPaddingTop() + paddedHeight ); if (measureAndCheckMinSize(topRect, startView) && measureAndCheckMinSize(bottomRect, endView)) { ArrayList rects = new ArrayList (); rects.add(topRect); rects.add(bottomRect); return rects; } } else if (featureBounds.top == 0) { // Vertical layout. Rect leftRect = new Rect( getPaddingLeft(), getPaddingTop(), position.left, getPaddingTop() + paddedHeight ); Rect rightRect = new Rect( position.right, getPaddingTop(), getPaddingLeft() + paddedWidth, getPaddingTop() + paddedHeight ); if (measureAndCheckMinSize(leftRect, startView) && measureAndCheckMinSize(rightRect, endView)) { ArrayList rects = new ArrayList (); rects.add(leftRect); rects.add(rightRect); return rects; } } } // You previously tried to fit the children and measure them. Since // they don't fit, measure again to update the stored values. measure(lastWidthMeasureSpec, lastHeightMeasureSpec); return null; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); lastWidthMeasureSpec = widthMeasureSpec; lastHeightMeasureSpec = heightMeasureSpec; } /** * Measures a child view and sees if it fits in the provided rect. * This method calls [View.measure] on the child view, which updates * its stored values for measured width and height. If the view ends up with * different values, measure again. */ private boolean measureAndCheckMinSize(Rect rect, View childView) { int widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), AT_MOST); childView.measure(widthSpec, heightSpec); return (childView.getMeasuredWidthAndState() & MEASURED_STATE_TOO_SMALL) == 0 && (childView.getMeasuredHeightAndState() & MEASURED_STATE_TOO_SMALL) == 0; } private boolean isValidFoldFeature(DisplayFeature displayFeature) { if (displayFeature instanceof FoldingFeature) { return SampleToolsKt.getFeaturePositionInViewRect(displayFeature, this, true) != null; } else { return false; } } }