نحوه ترسیم نماها توسط Android

امتحان کردن روش Compose
Jetpack Compose جعبه‌ابزار واسط کاربر توصیه‌شده برای Android است. درباره فازهای Compose بیشتر بدانید.

وقتی Activity فوکوس را دریافت می‌کند، چارچوب Android از 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 ۲۴۰ فراخوانی کند تا متوجه شود که فرزند View با عرض ۲۴۰ پیکسل می‌خواهد چقدر بلند باشد.
  • 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 استفاده کنید.

کاتلین

/**
 * 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
}

جاوا

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