Dessiner des vues via Android

Le framework Android demande à un élément Activity de dessiner sa mise en page lorsque cet élément Activity est sélectionné. Le framework Android gère la procédure de dessin, mais l'élément Activity doit fournir le nœud racine de sa hiérarchie de mise en page.

Le framework Android dessine le nœud racine de la mise en page, puis mesure et dessine l'arborescence de mise en page. Il dessine l'arborescence en parcourant l'arborescence et en affichant chaque View qui croise la région incorrecte. Chaque ViewGroup est responsable de la demande de dessin de chacun de ses enfants à l'aide de la méthode draw(), et chaque View est responsable du dessin lui-même. Comme l'arborescence est balayée dans le sens inverse, le framework dessine les parents avant leurs enfants (en d'autres termes, derrière), puis dessine les frères dans l'ordre dans lequel ils apparaissent dans l'arborescence.

Le framework Android dessine la mise en page dans un processus en deux étapes : mesure, puis mise en page. Le framework effectue l'étape de mise en page dans measure(int, int) et effectue un balayage de haut en bas de l'arborescence View. Chaque View transmet les spécifications de dimension vers le bas de l'arborescence pendant la récursion. À la fin de l'étape de mesure, chaque View stocke ses mesures. Le framework effectue la deuxième étape dans layout(int, int, int, int) et opère également de haut en bas. Au cours de cette étape, chaque parent est responsable du positionnement de tous ses enfants à l'aide des tailles calculées dans l'étape de mesure.

Les deux étapes du processus de mise en page sont décrites plus en détail dans les sections suivantes.

Lancer une étape de mesure

Lorsque la méthode measure() d'un objet View est renvoyée, définissez ses valeurs getMeasuredWidth() et getMeasuredHeight(), ainsi que celles qui correspondent à tous les descendants de cet objet View. Les valeurs de largeur et de hauteur mesurées d'un objet View doivent respecter les contraintes imposées par les parents de l'objet View. Cette approche permet de s'assurer qu'à la fin de la mesure, tous les parents accepteront toutes les mesures de leurs enfants.

Un parent View peut appeler measure() plusieurs fois pour ses enfants. Par exemple, le parent peut mesurer une fois les éléments enfants avec des dimensions non spécifiées afin de déterminer leur taille souhaitée. Si la somme des tailles non limitées des enfants est trop élevée ou trop faible, le parent peut appeler à nouveau measure() avec des valeurs qui limitent ces tailles.

La mesure utilise deux classes pour communiquer les dimensions. La classe ViewGroup.LayoutParams permet aux objets View de communiquer leurs tailles et leurs positions souhaitées. La classe ViewGroup.LayoutParams de base décrit la largeur et la hauteur souhaitées de View. Pour chaque dimension, elle peut spécifier l'une des valeurs suivantes :

  • Une dimension exacte.
  • MATCH_PARENT, ce qui signifie que la taille souhaitée pour View est la taille de son parent, moins la marge intérieure.
  • WRAP_CONTENT, ce qui signifie que la taille souhaitée pour View est juste assez grande pour encadrer son contenu, plus une marge intérieure.

Il existe des sous-classes de ViewGroup.LayoutParams pour différentes sous-classes de ViewGroup. Par exemple, RelativeLayout possède sa propre sous-classe de ViewGroup.LayoutParams, qui permet de centrer les objets enfants View horizontalement et verticalement.

Les objets MeasureSpec sont utilisés pour transmettre les exigences vers le bas de l'arborescence, du parent à l'enfant. Un élément MeasureSpec peut avoir l'un des trois modes suivants :

  • UNSPECIFIED : utilise le parent pour déterminer la dimension cible d'un élément View enfant. Par exemple, un élément LinearLayout peut appelermeasure() pour son enfant avec la hauteur définie sur UNSPECIFIED et une largeur "EXACTLY" de 240 pour déterminer la taille que l'élément View enfant souhaite avoir, sachant que la largeur est de 240 pixels.
  • EXACTLY : le parent utilise cet élément pour imposer une taille exacte à l'élément enfant. L'enfant doit utiliser cette taille et s'assurer que tous ses descendants s'adaptent à cette taille.
  • AT MOST : le parent utilise cet élément pour imposer une taille maximale à l'élément enfant. L'enfant doit s'assurer que tous ses descendants et lui-même respectent cette taille.

Lancer une étape de mise en page

Pour lancer une mise en page, appelez requestLayout(). Cette méthode est généralement appelée par un élément View sur lui-même, lorsqu'il estime qu'il ne peut plus tenir dans ses limites.

Implémenter une logique de mesure et de mise en page personnalisée

Si vous souhaitez implémenter une logique de mesure ou de mise en page personnalisée, remplacez les méthodes au niveau desquelles la logique est implémentée : onMeasure(int, int) et onLayout(boolean, int, int, int, int). Ces méthodes sont appelées respectivement par measure(int, int) et layout(int, int, int, int). N'essayez pas de remplacer les méthodes measure(int, int) ou layout(int, int). Ces deux méthodes sont toutes deux final. Vous ne pouvez donc pas les remplacer.

L'exemple suivant montre comment procéder dans la classe "SplitLayout" de l'application exemple WindowManager. Si SplitLayout comporte plusieurs vues enfants et que l'écran présente une ligne de flottaison, il positionne les deux vues enfants de chaque côté de la ligne de flottaison. L'exemple suivant montre un cas d'utilisation permettant de remplacer les mesures et la mise en page. Pour la production, utilisez SlidingPaneLayout si vous souhaitez que ce comportement s'applique.

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;
      }
   }
}