Cara Android menggambar Tampilan

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 untuk View yaitu ukuran induknya, tanpa padding.
  • WRAP_CONTENT, yang berarti ukuran yang lebih disukai untuk View 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 turunan View. Misalnya, LinearLayout mungkin memanggil measure() pada turunannya dengan tinggi yang ditetapkan ke UNSPECIFIED dan lebar EXACTLY 240 untuk mencari tahu tinggi turunan View 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
      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;
      }
   }
}