返回介绍

3.1 measure

发布于 2024-12-23 21:06:23 字数 9102 浏览 0 评论 0 收藏 0

在 LinearLayout 的 onMeasure() 里面,所有的测量都根据 mOrientation 这个 int 值来进行水平或者垂直的测量计算。

我们都知道,java 中 int 在初始化不分配值的时候,都是默认的 0,因此如果我们不指定 orientation,measure 则会按照水平方向来测量【水平 orientation=0/垂直 orientation=1】

接下来我们主要看看 measureVertical 方法,了解了垂直方向的测量之后,水平方向的也就不难理解了,为了篇幅,我们主要分析垂直方向的测量。

measureVertical 方法除去注释,大概 200 多行,因此我们分段分析。

方法主要分为三大块:

  • 一大堆变量
  • 一个主要的 for 循环来不断测量子控件
  • 其余参数影响以及根据是否有 weight 再次测量

3.1.1 一大堆变量

为何这里要说说变量,因为这些变量都会极大的影响到后面的测量,同时也是十分容易混淆的,所以这里需要贴一下。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

    // mTotalLength 作为 LinearLayout 成员变量,其主要目的是在测量的时候通过累加得到所有子控件的高度和(Vertical)或者宽度和(Horizontal)
    mTotalLength = 0;
    // maxWidth 用来记录所有子控件中控件宽度最大的值。
    int maxWidth = 0;
    // 子控件的测量状态,会在遍历子控件测量的时候通过 combineMeasuredStates 来合并上一个子控件测量状态与当前遍历到的子控件的测量状态,采取的是按位相或
    int childState = 0;
    
    /**
     * 以下两个最大宽度跟上面的 maxWidth 最大的区别在于 matchWidthLocally 这个参数
     * 当 matchWidthLocally 为真,那么以下两个变量只会跟当前子控件的左右 margin 和相比较取大值
     * 否则,则跟 maxWidth 的计算方法一样
     */
    // 子控件中 layout_weight<=0 的 View 的最大宽度
    int alternativeMaxWidth = 0;
    // 子控件中 layout_weight>0 的 View 的最大宽度
    int weightedMaxWidth = 0;
    // 是否子控件全是 match_parent 的标志位,用于判断是否需要重新测量
    boolean allFillParent = true;
    // 所有子控件的 weight 之和
    float totalWeight = 0;

    // 如您所见,得到所有子控件的数量,准确的说,它得到的是所有同级子控件的数量
    // 在官方的注释中也有着对应的例子
    // 比如 TableRow,假如 TableRow 里面有 N 个控件,而 LinearLayout(TableLayout 也是继承 LinearLayout 哦)下有 M 个 TableRow,那么这里返回的是 M,而非 M*N
    // 但实际上,官方似乎也只是直接返回 getChildCount(),起这个方法名的原因估计是为了让人更加的明白,毕竟如果是 getChildCount() 可能会让人误认为为什么没有返回所有(包括不同级)的子控件数量
    final int count = getVirtualChildCount();
    
    // 得到测量模式
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    // 当子控件为 match_parent 的时候,该值为 ture,同时判定的还有上面所说的 matchWidthLocally,这个变量决定了子控件的测量是父控件干预还是填充父控件(剩余的空白位置)。
    boolean matchWidth = false;
    
    boolean skippedMeasure = false;

    final int baselineChildIndex = mBaselineAlignedChildIndex;    
    final boolean useLargestChild = mUseLargestChild;

    int largestChildHeight = Integer.MIN_VALUE;
  }

这里有很多变量和值,事实上,直到现在,我依然没有完全弄明白这些值的意义。

在这一大堆变量里面,我们主要留意的是三个方面:

  • mTotalLength:这个就是最终得到的整个 LinearLayout 的高度(子控件高度累加及自身 padding)
  • 三个跟 width 相关的变量
  • weight 相关的变量

3.1.2 测量

通过 for 循环不断的得到子控件然后根据自己的定义进行赋值,这就是 LinearLayout 测量里面最重要的一步。

这里的代码比较长,去掉注释后有 100 行左右,因此这里采取重要地方注释结合文字描述来分析。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // ...接上面的一大堆变量
    for (int i = 0; i < count; ++i) {

      final View child = getVirtualChildAt(i);

      if (child == null) {
        // 目前而言,measureNullChild() 方法返回的永远是 0,估计是设计者留下来以后或许有补充的。
        mTotalLength += measureNullChild(i);
        continue;
      }
       
      if (child.getVisibility() == GONE) {
         // 同上,返回的都是 0。
         // 事实上这里的意思应该是当前遍历到的 View 为 Gone 的时候,就跳过这个 View,下一句的 continue 关键字也正是这个意思。
         // 忽略当前的 View,这也就是为什么 Gone 的控件不占用布局资源的原因。(毕竟根本没有分配空间)
        i += getChildrenSkipCount(child, i);
        continue;
      }

      // 根据 showDivider 的值(before/middle/end)来决定遍历到当前子控件时,高度是否需要加上 divider 的高度
      // 比如 showDivider 为 before,那么只会在第 0 个子控件测量时加上 divider 高度,其余情况下都不加
      if (hasDividerBeforeChildAt(i)) {
        mTotalLength += mDividerWidth;
      }

      final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
          child.getLayoutParams();
      // 得到每个子控件的 LayoutParams 后,累加权重和,后面用于跟 weightSum 相比较
      totalWeight += lp.weight;
      
      // 我们都知道,测量模式有三种:
      // * UNSPECIFIED:父控件对子控件无约束
      // * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是 Match_parent
      // * AT_Most:子控件为 wrap_content 的时候,测量值为 AT_MOST。
      
      // 下面的 if/else 分支都是跟 weight 相关
      if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
        // 这个 if 里面需要满足三个条件:
        // * LinearLayout 的高度为 match_parent(或者有具体值)
        // * 子控件的高度为 0
        // * 子控件的 weight>0
        // 这其实就是我们通常情况下用 weight 时的写法
        // 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
        skippedMeasure = true;
      } else {
        // 到这个分支,则需要对不同的情况进行测量
        int oldHeight = Integer.MIN_VALUE;

        if (lp.height == 0 && lp.weight > 0) {
          // 满足这两个条件,意味着父类即 LinearLayout 是 wrap_content,或者 mode 为 UNSPECIFIED
          // 那么此时将当前子控件的高度置为 wrap_content
          // 为何需要这么做,主要是因为当父类为 wrap_content 时,其大小实际上由子控件控制
          // 我们都知道,自定义控件的时候,通常我们会指定测量模式为 wrap_content 时的默认大小
          // 这里强制给定为 wrap_content 为的就是防止子控件高度为 0.
          oldHeight = 0;
          lp.height = LayoutParams.WRAP_CONTENT;
        }
        
        /**【1】*/
        // 下面这句虽然最终调用的是 ViewGroup 通用的同名方法,但传入的 height 值是跟平时不一样的
        // 这里可以看到,传入的 height 是跟 weight 有关,关于这里,稍后的文字描述会着重阐述
        measureChildBeforeLayout(
             child, i, widthMeasureSpec, 0, heightMeasureSpec,
             totalWeight == 0 ? mTotalLength : 0);

        // 重置子控件高度,然后进行精确赋值
        if (oldHeight != Integer.MIN_VALUE) {
           lp.height = oldHeight;
        }

        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        // getNextLocationOffset 返回的永远是 0,因此这里实际上是比较 child 测量前后的总高度,取大值。
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
             lp.bottomMargin + getNextLocationOffset(child));

        if (useLargestChild) {
          largestChildHeight = Math.max(childHeight, largestChildHeight);
        }
      }

      if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
         mBaselineChildTop = mTotalLength;
      }

      if (i < baselineChildIndex && lp.weight > 0) {
        throw new RuntimeException("A child of LinearLayout with index "
            + "less than mBaselineAlignedChildIndex has weight > 0, which "
            + "won't work.  Either remove the weight, or don't set "
            + "mBaselineAlignedChildIndex.");
      }

      boolean matchWidthLocally = false;
      
      // 还记得我们变量里又说到过 matchWidthLocally 这个东东吗
      // 当父类(LinearLayout)不是 match_parent 或者精确值的时候,但子控件却是一个 match_parent
      // 那么 matchWidthLocally 和 matchWidth 置为 true
      // 意味着这个控件将会占据父类(水平方向)的所有空间
      if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
        matchWidth = true;
        matchWidthLocally = true;
      }

      final int margin = lp.leftMargin + lp.rightMargin;
      final int measuredWidth = child.getMeasuredWidth() + margin;
      maxWidth = Math.max(maxWidth, measuredWidth);
      childState = combineMeasuredStates(childState, child.getMeasuredState());

      allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
      
      if (lp.weight > 0) {
        weightedMaxWidth = Math.max(weightedMaxWidth,
            matchWidthLocally ? margin : measuredWidth);
      } else {
        alternativeMaxWidth = Math.max(alternativeMaxWidth,
            matchWidthLocally ? margin : measuredWidth);
      }

      i += getChildrenSkipCount(child, i);
    }
  }

在代码中我注释了一部分,其中最值得注意的是 measureChildBeforeLayout() 方法。这个方法将会决定子控件可用的剩余分配空间。

measureChildBeforeLayout() 最终调用的实际上是 ViewGroup 的 measureChildWithMargins() ,不同的是,在传入高度值的时候(垂直测量情况下),会对 weight 进行一下判定

假如当前子控件的 weight 加起来还是为 0,则说明在当前子控件之前还没有遇到有 weight 的子控件,那么 LinearLayout 将会进行正常的测量,若之前遇到过有 weight 的子控件,那么 LinearLayout 传入 0。

那么 measureChildWithMargins() 的最后一个参数,也就是 LinearLayout 在这里传入的这个高度值是用来干嘛的呢?

如果我们追溯下去,就会发现,这个函数最终其实是为了结合父类的 MeasureSpec 以及 child 自身的 LayoutParams 来对子控件测量。而最后传入的值,在子控件测量的时候被添加进去。

  
   protected void measureChildWithMargins(View child,
      int parentWidthMeasureSpec, int widthUsed,
      int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
            + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
            + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }

在官方的注释中,我们可以看到这么一句:

* @param
* height
* Used
* Extra space that has been used up by the parent vertically (possibly by other children of the parent)

事实上,我们在代码中也可以很清晰的看到,在 getChildMeasureSpec() 中,子控件需要把父控件的 padding,自身的 margin 以及一个可调节的量三者一起测量出自身的大小。

那么假如在测量某个子控件之前,weight 一直都是 0,那么该控件在测量时,需要考虑在本控件之前的总高度,来根据剩余控件分配自身大小。而如果有 weight,那么就不考虑已经被占用的控件,因为有了 weight,子控件的高度将会在后面重新赋值。


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

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

发布评论

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