返回介绍

测量和布局过程

发布于 2024-12-23 22:09:58 字数 6395 浏览 0 评论 0 收藏 0

ViewGroup 由于需要兼顾子 View 的测量和布局,以及在部分情况下子 View 的测量反过来影响 ViewGroup 的测量(例如 ViewGroup 的宽高为 WRAP_CONTENT 的时候),整体的测量和布局需要经历比较复杂的过程。

首先从 onMeasure 方法开始,onMeasure 传入了两个作为测量依据的参数,widthMeasureSpec, heightMeasureSpec。这两个参数表面上是 int,但实际上是测量模式和父 View 要求的尺寸两个值组成的。这个 int 值的最高两位被用来储存测量模式,剩下的 30 位才是尺寸。测量模式有以下三种:

  • UNSPECIFIED 父 View 并没有强制要求当前 ViewGroup 遵从某个规范,当前 ViewGroup 可以是任何尺寸
  • EXACTLY 父 View 要求当前 ViewGroup 的宽度或者高度必须等于 MeasureSpec 传入的尺寸
  • AT_MOST 父 View 要求当前 ViewGroup 的宽度或者高度不能超过 MeasureSpec 传入的尺寸

然后 ViewGroup 根据传入的 MeasureSpec 以及自身对子 View 的排版规则对子 View 进行测量,在对子 View 的测量中又涉及到另一个类:LayoutParams。这个类相信大家都不陌生,不同的布局控件(RelativeLayout、LinearLayout、FrameLayout 等) 都有自己的一个 LayoutParams。里面包含了针对当前布局的一些参数,这些参数影响了 ViewGroup 对子 View 的测量。

LayoutParams 和 MeasureSpec 是比较容易搞混的,两个都是和测量布局有关的参数,每个控件都有针对于父 View 的 LayoutParams 成员变量,但父 View 对自身进行测量的时候又要传入 MeasureSpec。这两个参数的区别是什么呢?可以简单地理解为:LayoutParams 是 View 自己对自己的布局要求,而 MeasureSpec 是父 View 参考了 View 的 LayoutParams 后,提出来的对 View 的布局要求。这就像孩子吵着要跟父母拿钱买十颗糖吃(LayoutParams),父母经过深思熟虑后,决定只给孩子买五颗糖(MeasureSpec)。

ViewGroup 内部并没有真正处理子 View 的测量,因为 ViewGroup 本身并没有针对子 View 的规则限制。只有在具体的布局中,比如 RelativeLayout、LinearLayout,由于有子 View 的排版规则才会对子 View 进行测量。但是 ViewGroup 本身提供了默认的测量子 View 的方法:

measureChildren 根据 ViewGroup 自身 onMeasure 传入的 MeasureSpec 对所有子 View 进行测量,实际是遍历子 View 并调用了 measureChild 方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
     final int size = mChildrenCount;
     final View[] children = mChildren;
 		// 遍历子 View
     for (int i = 0; i < size; ++i) {
       final View child = children[i];
     	// 检测当前子 View 的 Visibility 状态,如果是 GONE 则跳过
       if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
         measureChild(child, widthMeasureSpec, heightMeasureSpec);
       }
     }
   }

​measureChild 根据 ViewGroup 自身 onMeasure 传入的 MeasureSpec 对某一个子 View 进行测量

protected void measureChild(View child, int parentWidthMeasureSpec,
      int parentHeightMeasureSpec) {
  		// 获取子 View 的 LayoutParams
    final LayoutParams lp = child.getLayoutParams();

 		// 计算并返回子 View 的 widthMeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight, lp.width);
  		// 计算并返回子 View 的 heightMeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);

  		// 调用子 View 的 measure 方法,measure 方法最终会调用子 View 的 onMeasure,具体的实现在 View 内
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }

​这里面实际核心的是 getChildMeasureSpec 方法,这是一个 ViewGroup 提供的根据 ViewGroup 自身的 MeasureSpec,ViewGroup 的 padding 以及子 View 的 LayoutParams 的 width/height 返回子 View 的 MeasureSpec 的方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  		// 用 MeasureSpec 提供的方法来获取具体的模式和尺寸
  		// (实际上是把高 2 位和低 30 位分开, mode = spec & 0xC0000000, size = spec & 0x3FFFFFFF)
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

  		// 由于上一步获取的是 ViewGroup 自身的 specSize,
  		// 而 ViewGroup 留给子 View 的区域是要扣除 padding 的,这边需要减去 padding
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // ViewGroup 自身的 MeasureSpec 为 EXACTLY 的时候
    case MeasureSpec.EXACTLY:
      if (childDimension >= 0) {
        // 子 View 的 LayoutParams 传入的尺寸既不是 MATCH_PARENT 也不是 WRAP_CONTENT 的时候,
        // 直接让子 View 的尺寸固定为传入的值
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子 View 的 LayoutParams 传入的为 MATCH_PARENT,即是和父 View 同样尺寸,
        	// 直接给 ViewGroup 的 size 值
        resultSize = size;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子 View 的 LayoutParams 传入的为 WRAP_CONTENT,但是 ViewGroup 自身是固定的尺寸,
        	// 这时候让子 View 在不超出 ViewGroup 的 size 的情况下自行决定大小
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

    // 传入的 ViewGroup 的 MeasureSpec 规定在给定的尺寸范围内自行决定大小,
    // 这通常是在 ViewGroup 本身的尺寸设置为 WRAP_CONTENT 的情况下传入
    // (参考上面 MeasureSpec.EXACTLY 的 case 里面 childDimension == LayoutParams.WRAP_CONTNET 的情况)
    case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
        //子 View 的 LayoutParams 传入的尺寸既不是 MATCH_PARENT 也不是 WRAP_CONTENT 的时候,
        //直接让子 View 的尺寸固定为传入的值
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子 View 的 LayoutParams 传入的为 MATCH_PARENT,但是 ViewGroup 自身的尺寸还未确定,
        	//只能让子 View 在 ViewGroup 的 size 范围内自行决定大小
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子 View 的 LayoutParams 传入的为 WRAP_CONTENT,
        // 这时候让子 View 在不超出 ViewGroup 的 size 的情况下自行决定大小
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

    case MeasureSpec.UNSPECIFIED:
      if (childDimension >= 0) {
        //子 View 的 LayoutParams 传入的尺寸既不是 MATCH_PARENT 也不是 WRAP_CONTENT 的时候,
        //直接让子 View 的尺寸固定为传入的值
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 在 ViewGroup 自身的 MeasureSpec 未定义的情况下(UNSPECIFIED),
        // 给子 View 也传未定义 mode(这边 sUseZeroUnspecifiedMeasureSpec)
        resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
        resultMode = MeasureSpec.UNSPECIFIED;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
        resultMode = MeasureSpec.UNSPECIFIED;
      }
      break;
    }
    // 用 MeasureSpec 类的 makeMeasureSpec 把 size 和 mode 拼装成一个 int 并返回
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }

当然这只是 ViewGroup 提供的默认的方法,并不是强制的要求以这个规则返回子 View 的 MeasureSpec。一些布局控件例如 RelativeLayout 会使用自己的子 View 的 MeasureSpec 生成规则。根据子 View 的 LayoutParams 来生成 MeasureSpec,并对子 View 进行测量后,ViewGroup(继承自 ViewGroup 的类) 就可以进行布局了。(布局对应的方法 onLayout 是一个抽象方法,在不同的 ViewGroup 的子类中有不一样的实现)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文