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

تجربة طريقة Compose
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدِم المقترَحة لنظام Android. مزيد من المعلومات عن مراحل Compose

يطلب إطار عمل 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;
      }
   }
}