Cómo dibuja vistas Android

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 para View es el tamaño de su elemento superior, menos el padding
  • WRAP_CONTENT, lo que significa que el tamaño preferido para el View 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 secundario View. Por ejemplo, un LinearLayout podría llamar a measure() en su elemento secundario con la altura establecida en UNSPECIFIED y un ancho de EXACTLY 240 para averiguar qué tan alto quiere que sea el elemento secundario View según un ancho de 240 píxeles
  • EXACTLY: 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ño
  • AT 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
      List splitPositions = 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;
      }
   }
}