เฟรมเวิร์ก Android จะขอให้
Activity วาดเลย์เอาต์เมื่อ
Activity ได้รับโฟกัส เฟรมเวิร์ก Android จะจัดการขั้นตอนการวาด แต่ Activity ต้องระบุโหนดรากของลำดับชั้นเลย์เอาต์
เฟรมเวิร์ก Android จะวาดโหนดรากของเลย์เอาต์ รวมถึงวัดและวาดแผนผังเลย์เอาต์ โดยจะวาดด้วยการเดินผ่านแผนผังและแสดงผล View แต่ละรายการที่ตัดกับภูมิภาคที่ไม่ถูกต้อง
ViewGroup แต่ละรายการมีหน้าที่
ขอให้วาดองค์ประกอบย่อยแต่ละรายการโดยใช้
draw()
วิธี และ View แต่ละรายการมีหน้าที่วาดตัวเอง เนื่องจากเฟรมเวิร์กจะข้ามผ่านแผนผังตามลำดับก่อนหน้า เฟรมเวิร์กจึงวาดองค์ประกอบระดับบนสุดก่อนองค์ประกอบระดับล่าง หรือพูดง่ายๆ ก็คือ ด้านหลังองค์ประกอบระดับล่าง และจะวาดองค์ประกอบที่อยู่ในระดับเดียวกันตามลำดับที่ปรากฏในแผนผัง
เฟรมเวิร์ก Android จะวาดเลย์เอาต์ในกระบวนการ 2 รอบ ได้แก่ รอบการวัดและรอบการจัดวาง เฟรมเวิร์กจะดำเนินการรอบการวัดใน measure(int, int) และดำเนินการข้ามผ่านแผนผัง View จากบนลงล่าง View แต่ละรายการจะส่งข้อกำหนดมิติข้อมูลลงในแผนผังระหว่างการเรียกซ้ำ เมื่อสิ้นสุดรอบการวัด View ทุกรายการจะจัดเก็บการวัด เฟรมเวิร์กจะดำเนินการรอบที่ 2 ใน layout(int, int, int, int) ซึ่งเป็นการดำเนินการจากบนลงล่างเช่นกัน ในรอบนี้ องค์ประกอบระดับบนสุดแต่ละรายการมีหน้าที่จัดตำแหน่งองค์ประกอบระดับล่างทั้งหมดโดยใช้ขนาดที่คำนวณในรอบการวัด
กระบวนการ 2 รอบของการจัดวางจะอธิบายไว้โดยละเอียดในส่วนต่อไปนี้
เริ่มรอบการวัด
เมื่อเมธอด measure() ของออบเจ็กต์ View แสดงผล ให้ตั้งค่า getMeasuredWidth() และ getMeasuredHeight() รวมถึงค่าขององค์ประกอบย่อยทั้งหมดของออบเจ็กต์ View ค่าความกว้างและความสูงที่วัดได้ของออบเจ็กต์ View ต้องเป็นไปตามข้อจำกัดที่กำหนดโดยองค์ประกอบระดับบนสุดของออบเจ็กต์ View ซึ่งจะช่วยให้มั่นใจได้ว่าเมื่อสิ้นสุดรอบการวัด องค์ประกอบระดับบนสุดทั้งหมดจะยอมรับการวัดขององค์ประกอบระดับล่างทั้งหมด
View ระดับบนสุดอาจเรียกใช้ measure() ในองค์ประกอบระดับล่างมากกว่า 1 ครั้ง เช่น องค์ประกอบระดับบนสุดอาจวัดองค์ประกอบระดับล่าง 1 ครั้งโดยไม่มีการระบุขนาดเพื่อกำหนดขนาดที่ต้องการ หากขนาดรวมขององค์ประกอบระดับล่างที่ไม่มีข้อจำกัดมีขนาดใหญ่หรือเล็กเกินไป องค์ประกอบระดับบนสุดอาจเรียกใช้ measure() อีกครั้งโดยใช้ค่าที่จำกัดขนาดขององค์ประกอบระดับล่าง
รอบการวัดใช้ 2 คลาสในการสื่อสารขนาด คลาส
ViewGroup.LayoutParams
เป็นวิธีที่ View ออบเจ็กต์สื่อสารขนาดและตำแหน่งที่ต้องการ คลาสฐาน
ViewGroup.LayoutParams จะอธิบายความกว้างและความสูงที่ต้องการของ
View โดยสามารถระบุค่าใดค่าหนึ่งต่อไปนี้สำหรับแต่ละมิติข้อมูล
- ขนาดที่แน่นอน
MATCH_PARENTซึ่งหมายความว่าขนาดที่ต้องการของViewคือขนาดขององค์ประกอบระดับบนสุดลบด้วยระยะห่างภายในWRAP_CONTENT, ซึ่งหมายความว่าขนาดที่ต้องการของViewมีขนาดใหญ่พอที่จะครอบคลุมเนื้อหาของ บวกกับระยะห่างภายใน
มีคลาสย่อยของ ViewGroup.LayoutParams สำหรับคลาสย่อยต่างๆ ของ
ViewGroup เช่น
RelativeLayout มี
คลาสย่อยของ ViewGroup.LayoutParams เป็นของตัวเอง ซึ่งมีความสามารถในการจัดกึ่งกลางออบเจ็กต์
View ขององค์ประกอบระดับล่างในแนวนอนและแนวตั้ง
ออบเจ็กต์ MeasureSpec ใช้เพื่อส่งข้อกำหนดจากองค์ประกอบระดับบนสุดไปยังองค์ประกอบระดับล่างในแผนผัง MeasureSpec สามารถอยู่ในโหมดใดโหมดหนึ่งต่อไปนี้
UNSPECIFIED: องค์ประกอบระดับบนสุดใช้โหมดนี้เพื่อกำหนดขนาดเป้าหมายของViewองค์ประกอบระดับล่าง เช่น aLinearLayoutอาจเรียกใช้measure()ในองค์ประกอบระดับล่างโดยตั้งค่าความสูงเป็นUNSPECIFIEDและความกว้าง ของEXACTLY240 เพื่อดูว่า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) เนื่องจากเมธอดทั้ง 2 นี้เป็น final จึงลบล้างไม่ได้
ตัวอย่างต่อไปนี้แสดงวิธีดำเนินการในคลาส
`SplitLayout`
จากแอปตัวอย่าง
WindowManager
หาก SplitLayout มีมุมมองย่อยตั้งแต่ 2 รายการขึ้นไปและจอแสดงผลมีรอยพับ
ระบบจะวางมุมมองย่อย 2 รายการไว้ที่ด้านใดด้านหนึ่งของรอยพับ ตัวอย่างต่อไปนี้แสดงกรณีการใช้งานสำหรับการลบล้างการวัดและการจัดวาง แต่สำหรับการใช้งานจริง ให้ใช้ 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; } } }