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

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

فریم ورک اندروید گره ریشه طرح بندی را ترسیم می کند و درخت layout را اندازه گیری و ترسیم می کند. با راه رفتن روی درخت و رندر کردن هر 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 به اندازه کافی بزرگ است تا محتوای آن را در بر بگیرد، به علاوه padding.

زیر کلاس های 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 استفاده کنید.

کاتلین

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