Framework Android meminta
Activity
untuk menggambar tata letaknya saat
Activity
menerima fokus. Framework Android akan menangani prosedur menggambar, tetapi
Activity
harus menyediakan
node root hierarki tata letaknya.
Framework Android menggambar node root tata letak dan mengukur serta menggambar struktur tata letak. Node root
digambar dengan mengikuti struktur dan merender setiap
View
yang berpotongan dengan area yang tidak valid.
Setiap ViewGroup
bertanggung jawab
untuk meminta agar setiap turunannya digambar, menggunakan metode draw()
,
dan setiap View
bertanggung jawab untuk menggambar dirinya sendiri. Karena struktur ini dilalui
sesuai urutan yang ditentukan sebelumnya, framework menggambar induk sebelum—dengan kata lain, di belakang—turunannya, dan menggambar tampilan yang setara sesuai urutan kemunculannya di struktur.
Framework Android menggambar tata letak dalam proses dua tahap: tahap pengukuran dan tahap tata letak. Framework
ini melakukan tahap pengukuran dalam
measure(int, int)
dan
melakukan urutan struktur View
dari atas ke bawah. Setiap View
mendorong spesifikasi dimensi ke bawah dalam struktur selama rekursi. Pada akhir tahap pengukuran, setiap
View
akan menyimpan pengukurannya. Framework ini melakukan tahap kedua di
layout(int, int, int, int)
dan juga dari atas ke bawah. Selama tahap ini, setiap induk bertanggung jawab untuk menentukan posisi semua turunannya
berdasarkan ukuran yang dihitung selama tahap pengukuran.
Dua tahap proses tata letak dijelaskan secara lebih mendetail di bagian berikut.
Memulai tahap pengukuran
Saat metode measure()
dari objek View
ditampilkan, tetapkan nilai
getMeasuredWidth()
dan
getMeasuredHeight()
-nya,
beserta nilai untuk semua turunan objek View
tersebut. Nilai tinggi dan lebar
terukur objek View
harus mengikuti batasan yang diberlakukan oleh
induk objek View
. Hal ini membantu memastikan bahwa pada akhir tahap pengukuran, semua induk
menyetujui ukuran untuk semua turunannya.
View
induk dapat memanggil measure()
lebih dari sekali di lokasi turunannya. Misalnya
induk dapat mengukur turunannya sekali dengan dimensi yang tidak ditentukan untuk menentukan
ukuran yang diinginkan. Jika jumlah ukuran turunan yang tidak dibatasi terlalu besar atau terlalu kecil, induk
mungkin memanggil measure()
lagi dengan nilai yang membatasi ukuran turunan.
Tahap pengukuran menggunakan dua class untuk mengomunikasikan dimensi. Class
ViewGroup.LayoutParams
adalah cara objek View
menyampaikan ukuran dan posisi yang diinginkan. Class
ViewGroup.LayoutParams
dasar menjelaskan lebar dan tinggi
View
yang diinginkan. Untuk setiap dimensi dapat menentukan salah satu hal berikut:
- Dimensi yang tepat.
MATCH_PARENT
, yang berarti ukuran yang lebih disukai untukView
yaitu ukuran induknya, tanpa padding.WRAP_CONTENT
, yang berarti ukuran yang lebih disukai untukView
yaitu yang cukup besar untuk menampung isinya, plus padding.
Terdapat subclass ViewGroup.LayoutParams
untuk subclass yang berbeda dari ViewGroup
. Misalnya,
RelativeLayout
memiliki subclass-nya sendiri,
yaitu ViewGroup.LayoutParams
, yang memiliki kemampuan untuk menempatkan objek View
turunan di tengah-tengah baik secara horizontal maupun vertikal.
Objek MeasureSpec
digunakan untuk mendorong
persyaratan ke bawah dalam struktur dari induk ke turunan. MeasureSpec
dapat berupa salah satu dari
tiga mode berikut:
UNSPECIFIED
: induk menggunakan ini untuk menentukan dimensi target turunanView
. Misalnya,LinearLayout
mungkin memanggilmeasure()
pada turunannya dengan tinggi yang ditetapkan keUNSPECIFIED
dan lebarEXACTLY
240 untuk mencari tahu tinggi turunanView
yang diinginkan, dengan lebar 240 piksel.EXACTLY
: induk menggunakan ini untuk menerapkan ukuran yang tepat pada turunan. Turunan harus menggunakan ukuran ini, dan menjamin bahwa semua turunannya akan cocok dalam ukuran tersebut.AT MOST
: induk menggunakan ini untuk menerapkan ukuran maksimum pada turunan. Turunan harus menjamin bahwa turunan tersebut beserta semua turunannya akan cocok dalam ukuran ini.
Memulai tahap tata letak
Untuk memulai sebuah tata letak, panggil
requestLayout()
. Metode
ini biasanya dipanggil oleh View
di lokasinya sendiri saat yakin bahwa metode tersebut tidak cocok lagi
dalam batasnya saat ini.
Mengimplementasikan logika pengukuran dan tata letak kustom
Jika Anda ingin menerapkan logika pengukuran atau tata letak kustom, ganti metode yang menerapkan logika
:
onMeasure(int, int)
dan
onLayout(boolean, int, int, int, int)
.
Metode ini masing-masing dipanggil oleh measure(int, int)
dan
layout(int, int, int, int)
. Jangan mencoba mengganti
metode measure(int, int)
atau layout(int, int)
—keduanya adalah
metode final
, sehingga tidak dapat diganti.
Contoh berikut menunjukkan cara melakukannya di class
`SplitLayout`
dari aplikasi contoh
WindowManager. Jika SplitLayout
memiliki dua tampilan turunan atau lebih, dan layar memiliki lipatan,
maka metode ini akan memosisikan dua tampilan turunan di kedua sisi lipatan. Contoh berikut menunjukkan kasus
penggunaan untuk mengganti pengukuran dan tata letak, tetapi untuk produksi, gunakan
SlidingPaneLayout
jika Anda ingin perilaku ini.
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; } } }