كيف يجذب Android المشاهدات

يطلب إطار عمل Android من Activity رسم التنسيق عندما يتم التركيز على Activity. يعالج إطار عمل Android إجراء الرسم، ولكن يجب أن يوفّر Activity العقدة الجذرية للتسلسل الهرمي للتنسيق.

يرسم إطار عمل Android العقدة الأساسية للتنسيق ويقيسه ويرسم شجرة التنسيق. يرسم من خلال المشي على الشجرة وعرض كل View يتداخل مع المنطقة غير الصالحة. وتكون كل ViewGroup مسؤولة عن طلب رسم كل عنصر من عناصرها الثانوية باستخدام طريقة draw()، ويكون كل View مسؤولاً عن رسم نفسه. وبما أنّه يتم تجاوز الشجرة حسب الطلب المسبق، يجذب إطار العمل الأهل قبل أولادهم (أي الخلف)، ويرسم الأشقاء بالترتيب الذي يظهرون به في الشجرة.

يرسم إطار عمل Android التخطيط من خلال عملية من مرّتين: تمريرة قياس وتمرير تخطيط. ينفِّذ إطار العمل مقياس الاجتياز في measure(int, int) ويجري مسحًا من أعلى لأسفل لشجرة View. يدفع كل View مواصفات الأبعاد إلى أسفل الشجرة أثناء التكرار. في نهاية المقياس، يتم تخزين كل قياساته في كل View. يؤدي إطار العمل التمرير الثاني في layout(int, int, int, int) وهو أيضًا من أعلى إلى أسفل. خلال هذه التصريحات، يكون كل أب مسئولاً عن تحديد موقع جميع عناصره الثانوية باستخدام المقاسات المحسوبة في مسار القياس.

يتم وصف مرحلتي عملية التخطيط بمزيد من التفصيل في الأقسام التالية.

بدء إقرار القياس

عند عرض طريقة measure() لكائن View، اضبط قيمتَي getMeasuredWidth() و getMeasuredHeight() مع قيمهما لكل العناصر التابعة لكائن View. إنّ قيم العرض الذي تم قياسه وقيم الارتفاع التي تم قياسها لكائن View يجب أن تتوافق مع القيود التي يفرضها العنصر الرئيسي للكائن View. يساعد هذا في التأكد من قبول جميع الأهل لجميع قياسات أطفالهم في نهاية المقياس.

قد يتصل أحد الوالدَين View بـ measure() أكثر من مرة على عناصره الثانوية. على سبيل المثال، يمكن أن يقيس الوالد الأطفال مرة واحدة بأبعاد غير محدّدة لتحديد المقاسات المفضّلة لديهم. إذا كان مجموع المقاسات غير المحدودة للأطفال كبيرًا جدًا أو صغيرًا جدًا، قد يسمّى الوالد measure() مرة أخرى باستخدام القيم التي تحدّ من مقاسات الأطفال.

يستخدم تصريح القياس فئتين لتوصيل الأبعاد. الفئة ViewGroup.LayoutParams هي طريقة توصيل عناصر View بأحجامها ومواضعها المفضّلة. تصف فئة ViewGroup.LayoutParams الأساسية العرض والارتفاع المفضّلَين في View. بالنسبة إلى كل سمة، يمكن أن تحدد واحدًا مما يلي:

  • بُعد دقيق.
  • MATCH_PARENT، أي أنّ الحجم المفضّل لـ View هو حجم العنصر الرئيسي بدون المساحة المتروكة.
  • WRAP_CONTENT، أي أنّ المقاس المفضّل لـ View كبير بما يكفي لاحتواء المحتوى، بالإضافة إلى المساحة المتروكة.

تتوفّر فئات فرعية من ViewGroup.LayoutParams للفئات الفرعية المختلفة من ViewGroup. على سبيل المثال، يحتوي RelativeLayout على فئته الفرعية ViewGroup.LayoutParams الخاصة به، وهي تتيح إمكانية توسيط عناصر View الثانوية أفقيًا وعموديًا.

تُستخدم كائنات MeasureSpec لدفع المتطلبات إلى أسفل الشجرة من الأصل إلى التابع. يمكن وضع MeasureSpec بطريقة من ثلاثة أوضاع:

  • UNSPECIFIED: يستخدم العنصر الرئيسي هذه السمة لتحديد السمة المستهدفة لعنصر View الثانوي. على سبيل المثال، قد يسمّى LinearLayout بالرمز measure() في عنصره الفرعي مع ضبط الارتفاع على UNSPECIFIED وعرض EXACTLY 240 لمعرفة طول الطفل View بناءً على العرض الذي يبلغ 240 بكسل.
  • EXACTLY: يستخدم أحد الوالدَين هذه السمة لفرض المقاس بدقة على الطفل. ويجب أن يستخدم الطفل هذا المقاس مع ضمان أن تكون جميع عناصره الفرعية مناسبة لهذا المقاس.
  • AT MOST: يستخدم أحد الوالدَين هذه السمة لفرض حد أقصى لحجم الطفل. ويجب أن يضمن الطفل أن يكون هذا المقاس هو وجميع كل توابعه التابعة له.

بدء تمرير التخطيط

لبدء تصميم، يمكنك استدعاء requestLayout(). تُسمّى هذه الطريقة عادةً باستخدام السمة View نفسها إذا رأت أنّها لم تعُد تستوفي حدودها.

تنفيذ منطق تصميم وقياس مخصّص

إذا كنت تريد تنفيذ منطق تنسيق أو قياس مخصّص، يمكنك إلغاء الطُرق التي يتم فيها تنفيذ المنطق: onMeasure(int, int) و onLayout(boolean, int, int, int, int). يتم استدعاء هاتين الطريقتين بواسطة measure(int, int) وlayout(int, int, int, int)، على التوالي. لا تحاول إلغاء الطريقتَين measure(int, int) أو layout(int, int)، لأنّ كلتا الطريقتين final، لذلك لا يمكن إلغاءهما.

يوضِّح المثال التالي كيفية إجراء ذلك في فئة `SplitLayout` من نموذج تطبيق WindowManager. إذا كان SplitLayout يشتمل على عرضَين ثانويَين أو أكثر، وكانت الشاشة مزوّدة بطي، يتم وضع العرضَين الفرعيَين على جانبَي الجزء المرئي من الصفحة. يوضح المثال التالي حالة استخدام لتجاوز القياس والتنسيق، ولكن بالنسبة للإنتاج، استخدم SlidingPaneLayout إذا كنت تريد هذا السلوك.

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