El framework de Android le solicita a una Activity
que dibuje su diseño cuando Activity
recibe el foco. El framework de Android controla el procedimiento de dibujo, pero la Activity
debe proporcionar el nodo raíz de su jerarquía de diseño.
El framework de Android dibuja el nodo raíz del diseño y mide y dibuja el árbol de diseño. Para dibujar, recorre el árbol y renderiza cada View
que se interseca con la región no válida.
Cada ViewGroup
es responsable de solicitar que se dibuje cada uno de sus elementos secundarios, con el método draw()
, y cada View
es responsable de dibujarlo. Debido a que el árbol se recorre por adelantado, el framework dibuja los elementos superiores antes (en otras palabras, detrás) de sus elementos secundarios y dibuja los elementos del mismo nivel en el orden en que aparecen en el árbol.
El framework de Android dibuja el diseño en un proceso de dos pases: un pase de medición y un pase de diseño. El framework realiza el pase de medición en measure(int, int)
y realiza un recorrido de arriba abajo del árbol View
. Cada View
envía las especificaciones de dimensión hacia abajo en el árbol durante la recursividad. Al final del pase de medición, cada View
almacena sus mediciones. El framework realiza el segundo pase en layout(int, int, int, int)
y también es de arriba abajo. Durante ese pase, cada elemento superior es responsable de posicionar todos sus elementos secundarios utilizando los tamaños procesados en el pase de medición.
Los dos pases del proceso de diseño se describen con más detalle en las siguientes secciones.
Cómo iniciar un pase de medición
Cuando se muestra el método measure()
de un objeto View
, configura sus valores getMeasuredWidth()
y getMeasuredHeight()
, junto con todos los de los elementos subordinados del objeto View
. Los valores medidos de ancho y altura de un objeto View
deben respetar las restricciones impuestas por los elementos superiores del objeto View
. Eso ayuda a que, al final del pase de medición, todos los elementos superiores acepten todas las medidas de sus elementos secundarios.
Un elemento superior View
podría llamar a measure()
más de una vez en sus elementos secundarios. Por ejemplo, el elemento superior podría medir los elementos secundarios una vez con dimensiones no especificadas para determinar sus tamaños preferidos. Si la suma de los tamaños no restringidos de los elementos secundarios es demasiado grande o demasiado pequeña, el elemento superior podría volver a llamar a measure()
con valores que restrinjan los tamaños de los elementos secundarios.
El pase de medición utiliza dos clases para comunicar dimensiones. La clase ViewGroup.LayoutParams
es la forma en que los objetos View
comunican sus posiciones y tamaños preferidos. La clase ViewGroup.LayoutParams
base describe la altura y el ancho preferidos de View
. Para cada dimensión, puede especificar una de las siguientes opciones:
- Una dimensión exacta.
MATCH_PARENT
, lo que significa que el tamaño preferido paraView
es el tamaño de su elemento superior, menos el paddingWRAP_CONTENT
, lo que significa que el tamaño preferido para elView
es lo suficientemente grande como para encerrar su contenido, más el padding
Hay subclases de ViewGroup.LayoutParams
para diferentes subclases de ViewGroup
. Por ejemplo, RelativeLayout
tiene su propia subclase de ViewGroup.LayoutParams
, que incluye la capacidad de centrar objetos View
secundarios en sentido horizontal y vertical.
Los objetos MeasureSpec
se utilizan para enviar los requisitos hacia abajo del árbol, de elemento superior a elemento secundario. Una MeasureSpec
puede estar en uno de estos tres modos:
UNSPECIFIED
: el elemento superior usa esto para determinar la dimensión objetivo de un elemento secundarioView
. Por ejemplo, unLinearLayout
podría llamar ameasure()
en su elemento secundario con la altura establecida enUNSPECIFIED
y un ancho deEXACTLY
240 para averiguar qué tan alto quiere que sea el elemento secundarioView
según un ancho de 240 píxelesEXACTLY
: el elemento superior usa esto para imponer un tamaño exacto al elemento secundario. El elemento secundario debe usar este tamaño y garantizar que todos sus elementos subordinados se ajusten a este tamañoAT MOST
: el elemento superior usa esto para imponer un tamaño máximo al elemento secundario. El elemento secundario debe garantizar que él y todos sus elementos subordinados se ajusten a este tamaño
Cómo iniciar un pase de diseño
Para iniciar un diseño, llama a requestLayout()
. Por lo general, una View
llama a este método cuando cree que ya no puede caber dentro de sus límites.
Cómo implementar una lógica de medición y diseño personalizada
Si quieres implementar una lógica de medición o diseño personalizada, anula los métodos en los que se implementa la lógica: onMeasure(int, int)
y onLayout(boolean, int, int, int, int)
.
measure(int, int)
y layout(int, int, int, int)
llaman a estos métodos, respectivamente. No intentes anular los métodos measure(int, int)
o layout(int, int)
, ya que ambos son final
, por lo que no se pueden anular.
En el siguiente ejemplo, se muestra cómo hacerlo en la clase "SplitLayout" de la app de ejemplo de WindowManager. Si SplitLayout
tiene dos o más vistas secundarias y la pantalla tiene un pliegue, posiciona las dos vistas secundarias en cualquiera de los dos lados de la línea de plegado En el siguiente ejemplo, se muestra un caso de uso para anular la medición y el diseño, pero para la producción, usa SlidingPaneLayout
si deseas este comportamiento.
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; } } }