當 Activity
收到焦點時,Android 架構會要求 Activity
繪製自身的版面配置。這個架構會處理繪圖程序,但 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()
。
測量過程會透過兩個類別來傳送維度。View
物件會透過 ViewGroup.LayoutParams
類別傳送自身偏好的大小和位置。基本 ViewGroup.LayoutParams
類別則說明 View
偏好的寬度和高度。對於每個維度,此過程可以指定下列任一項目:
- 確切維度。
MATCH_PARENT
,表示View
的偏好大小是其父項大小減去邊框間距。WRAP_CONTENT
,表示View
的偏好大小恰巧可容納其內容以及邊框間距。
ViewGroup
的不同子類別也有適用的 ViewGroup.LayoutParams
子類別。舉例來說,RelativeLayout
有專屬的 ViewGroup.LayoutParams
子類別,能夠水平和垂直將子項 View
物件置中。
MeasureSpec
物件的用途是在樹狀結構中將要求從父項向下推到子項。MeasureSpec
可為以下三種模式之一:
UNSPECIFIED
:父項會使用這個模式來決定子項View
的目標維度。舉例來說,LinearLayout
可能會在其子項 (高度和寬度分別設為UNSPECIFIED
和EXACTLY
) 上呼叫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; } } }