چارچوب Android از یک Activity
میخواهد که طرحبندی خود را هنگامی که Activity
فوکوس میکند، ترسیم کند. فریم ورک اندروید رویه ترسیم را انجام می دهد، اما Activity
باید گره ریشه سلسله مراتب طرح بندی خود را ارائه دهد.
فریم ورک اندروید گره ریشه طرح بندی را ترسیم می کند و درخت layout را اندازه گیری و ترسیم می کند. با راه رفتن روی درخت و رندر کردن هر View
که ناحیه نامعتبر را قطع می کند، ترسیم می کند. هر ViewGroup
با استفاده از متد draw()
مسئول درخواست ترسیم هر یک از فرزندانش است و هر View
مسئول ترسیم خود است. از آنجایی که درخت به صورت پیشترتیب پیمایش میشود، چارچوب والدین را از قبل – به عبارت دیگر، پشت سر – فرزندانشان میکشد و خواهر و برادرها را به ترتیبی که در درخت ظاهر میشوند ترسیم میکند.
فریم ورک اندروید طرحبندی را در یک فرآیند دو پاسی ترسیم میکند: پاس اندازهگیری و پاس طرح. فریم ورک، اندازه گیری را در measure(int, int)
انجام می دهد و یک پیمایش از بالا به پایین درخت View
را انجام می دهد. هر View
مشخصات ابعاد را در طول بازگشت به درخت فشار می دهد. در پایان اندازه گیری، هر View
اندازه گیری های خود را ذخیره می کند. فریم ورک پاس دوم را در layout(int, int, int, int)
انجام می دهد و همچنین از بالا به پایین است. در طول این پاس، هر والدین مسئول قرار دادن همه فرزندان خود با استفاده از اندازه های محاسبه شده در مجوز اندازه گیری هستند.
دو گذر از فرآیند چیدمان با جزئیات بیشتر در بخش های بعدی توضیح داده شده است.
راه اندازی مجوز اندازه گیری
هنگامی که متد measure()
meter یک شی View
برمی گردد، مقادیر getMeasuredWidth()
و getMeasuredHeight()
آن را به همراه مقادیر مربوط به همه نوادگان شی View
تنظیم کنید. مقادیر عرض و ارتفاع اندازه گیری شده یک شی View
باید به محدودیت های اعمال شده توسط والدین شی View
احترام بگذارد. این کمک می کند تا اطمینان حاصل شود که در پایان اندازه گیری، همه والدین تمام اندازه گیری های فرزندان خود را می پذیرند.
View
والدین ممکن است بیش از یک بار measure()
روی فرزندان خود فراخوانی کند. به عنوان مثال، والدین ممکن است یک بار کودکان را با ابعاد نامشخص اندازه گیری کنند تا اندازه های دلخواه آنها را تعیین کنند. اگر مجموع اندازههای محدود نشده بچهها خیلی بزرگ یا خیلی کوچک باشد، والدین ممکن است با مقادیری که اندازههای بچهها را محدود میکنند measure()
را دوباره فراخوانی کند.
اندازه گذر از دو کلاس برای ارتباط ابعاد استفاده می کند. کلاس ViewGroup.LayoutParams
نحوه ارتباط اشیاء View
با اندازه ها و موقعیت های دلخواه خود است. کلاس ViewGroup.LayoutParams
پایه، عرض و ارتفاع ترجیحی View
را توصیف می کند. برای هر بعد، می تواند یکی از موارد زیر را مشخص کند:
- یک بعد دقیق
-
MATCH_PARENT
، به این معنی که اندازه ترجیحی برایView
، اندازه والد آن است، منهای padding. -
WRAP_CONTENT
، به این معنی که اندازه ترجیحیView
به اندازه کافی بزرگ است تا محتوای آن را در بر بگیرد، به علاوه padding.
زیر کلاس های ViewGroup.LayoutParams
برای زیر کلاس های مختلف ViewGroup
وجود دارد. به عنوان مثال، RelativeLayout
زیر کلاس خود را از ViewGroup.LayoutParams
دارد که شامل قابلیت وسط قرار دادن اشیاء View
کودک به صورت افقی و عمودی است.
اشیاء MeasureSpec
برای فشار دادن نیازمندی ها به درخت از والدین به فرزند استفاده می شوند. MeasureSpec
می تواند در یکی از سه حالت باشد:
-
UNSPECIFIED
: والد از این برای تعیین بعد هدفView
فرزند استفاده می کند. به عنوان مثال، یکLinearLayout
ممکن استmeasure()
روی فرزند خود با ارتفاع تعیین شده رویUNSPECIFIED
و پهنایEXACTLY
240 فراخوانی کند تا بفهمد که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)
را لغو کنید - هر دوی این روش ها final
هستند، بنابراین نمی توان آنها را نادیده گرفت.
مثال زیر نحوه انجام این کار را در کلاس 'SplitLayout' از برنامه نمونه WindowManager نشان می دهد. اگر SplitLayout
دو یا چند نمای کودک داشته باشد و صفحه نمایش دارای یک تا شود، آنگاه دو نمای کودک را در دو طرف تاشو قرار می دهد. مثال زیر یک مورد استفاده برای نادیده گرفتن اندازهگیری و طرحبندی را نشان میدهد، اما برای تولید، اگر این رفتار را میخواهید، از SlidingPaneLayout
استفاده کنید.
کاتلین
/** * 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 }
جاوا
/** * 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; } } }