اندروید چگونه نماها را ترسیم می کند

روش نوشتن را امتحان کنید
Jetpack Compose ابزار رابط کاربری پیشنهادی برای اندروید است. درباره مراحل Compose اطلاعات کسب کنید.

چارچوب اندروید از یک Activity می‌خواهد که طرح‌بندی خود را زمانی که Activity فوکوس را دریافت می‌کند، رسم کند. چارچوب اندروید روال رسم را مدیریت می‌کند، اما Activity باید گره ریشه سلسله مراتب طرح‌بندی خود را ارائه دهد.

چارچوب اندروید گره ریشه طرح‌بندی را رسم می‌کند و درخت طرح‌بندی را اندازه‌گیری و ترسیم می‌کند. این کار با پیمایش درخت و رندر کردن هر View که ناحیه نامعتبر را قطع می‌کند، انجام می‌شود. هر ViewGroup مسئول درخواست ترسیم هر یک از فرزندانش با استفاده از متد draw() است و هر View مسئول ترسیم خود است. از آنجایی که درخت به صورت پیش‌ترتیب پیمایش می‌شود، چارچوب، والدین را قبل از - به عبارت دیگر، پشت - فرزندانشان ترسیم می‌کند و خواهر و برادرها را به ترتیبی که در درخت ظاهر می‌شوند، ترسیم می‌کند.

چارچوب اندروید طرح‌بندی را در یک فرآیند دو مرحله‌ای ترسیم می‌کند: یک مرحله اندازه‌گیری و یک مرحله چیدمان. چارچوب، مرحله اندازه‌گیری را در 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 ، اندازه والد آن، منهای padding است.
  • 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;
      }
   }
}