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 untukViewyaitu ukuran induknya, tanpa padding.WRAP_CONTENT, yang berarti ukuran yang lebih disukai untukViewyaitu 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,LinearLayoutmungkin memanggilmeasure()pada turunannya dengan tinggi yang ditetapkan keUNSPECIFIEDdan lebarEXACTLY240 untuk mencari tahu tinggi turunanViewyang 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; } } }