安卓自定义view(二) - 测量

作者: 小怪兽大作战 | 来源:发表于2019-02-26 16:56 被阅读3次

    view的测量过程

    之所以先讲view的测量过程,是因为ViewGroup测量的时候是先把他的所有子view测量完成后才能测量viewgroup自身,view的measure是viewGroup来调用的。在view的测量过程中,parentView首先会调用getChildMeasureSpec(int spec, int padding, int childDimension)

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高
     * 为子View生成最合适的MeasureSpec
     *
     * @param spec 父容器的MeasureSpec
     * @param padding 父容器的内间距(padding)加上子View的外间距(margin)
     * @param childDimension 子View的LayoutParams中封装的width/height
     * @return 子View的MeasureSpec
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // ① 对父容器的MeasureSpec进行解包
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        // ② 减去间距,得到最大可用空间
        int size = Math.max(0, specSize - padding);
    
        // 记录子View最终的大小和测量模式
        int resultSize = 0;
        int resultMode = 0;
    
        switch (specMode) {
        // ③ 父容器是精准测量模式
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {               //如果子view在LayoutParam中指定了大小,那么子view的resultSize 就是该大小,模式是EXACTLY
                resultSize = childDimension; 
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {       //如果子view中LayoutParams是MATCH_PARENT,则view的大小等于最大可用大小,测量模式是EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {   //如果子view中LayoutParams是WRAP_CONTENT,则view的大小等于最大可用大小,测量模式是AT_MOST
    
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
         // ④ 父容器指定了一个最大可用的空间
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // ⑤ 父容器不对子View的大小作出限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        // ⑥ 将最终的size和mode打包为子View需要的MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    

    计算该view的WidthMeasureSpec和HeightMeasureSpec,然后调用view的measure();

    public final void measure(int widthMeasureSpec, int  heightMeasureSpec) {
    ...
    
            onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
    }
    

    measure方法又调用了onMeasure方法,view.OnMeasure()才是真正完成了view的测量。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     setMeasuredDimension(   //设置测量的结果
        getDefaultSize(getSuggestedMinimumWidth(),
                                    widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(),
                                     heightMeasureSpec));
    }
    

    在view.onMeasure中调用setMeasuredDimension来保存测量的结果,那么测量结果是怎么来的呢?我们继续看getDefaultSize();

    public static int getDefaultSize(int size, int measureSpec) {
      int result = size;
     //1、获得MeasureSpec的mode
      int specMode = MeasureSpec.getMode(measureSpec);
     //2、获得MeasureSpec的specSize
      int specSize = MeasureSpec.getSize(measureSpec);
    
      switch (specMode) {
      case MeasureSpec.UNSPECIFIED:
        //这个我们先不看他
          result = size;
          break;
      case MeasureSpec.AT_MOST:
      case MeasureSpec.EXACTLY:
      //3、可以看到,最终返回的size就是我们MeasureSpec中测量得到的size
          result = specSize;
          break;
      }
      return result;
    }
    

    在getDefaultSize中传入了两个参数,一个是最小大小,一个是MeasureSpec。首先获得MeasureSpec中的测量模式和暂定大小,然后根据测量模式来返回不同的测量结果。如果测量模式是UNSPECIFIED,则测量的结果就是最小的大小,如果测量模式是AT_MOST或者EXACTLY,测量的大小都是MeasureSpec中的大小。
    而MeasureSpec中的大小是viewgroup调用getChildMeasureSpec生成的,查看getChildMeasureSpec的逻辑我们会看出,如果子view的layoutParem是warp_content,那么测量结果的测量模式就是AT_MOST,测量大小就是父容器的最大可用空间,所以我们在继承view自定义view的时候,如果重写onMeasure,那么设置LayoutParam设置warp_content是不能用的,大小始终是可用的最大大小。

    那么getDefaultSize的最小大小是怎么来的呢?是通过getSuggestedMinimumWidth()和getSuggestedMinimumHeight()得到,我们继续看getSuggestedMinimumWidth。

    protected int getSuggestedMinimumWidth() {
      return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
    

    在getSuggestedMinimumWidth中,首先判断该view有没有背景,如果没有背景,返回android:minWidth(如果没有设置android:minWidth,就默认是0),如果有背景,就返回背景大小和android:minWidth中的最大值。

    最后,onMeasure讲测量的结果调用

    getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
    

    保存起来,这样一个view就算测量完成了。
    画个图来总结一下。
    首先viewGroup会计算子view的MeasureSpec,然后讲所计算的MeasureSpec传入子view的measure中,子view开始测量自身。measure又会调用onMeasure来完成真正的测量过程。onMeasure会调用setMeasureDemension来保存测量的结果,测量结果是通过getDefaultSize来计算的,getDefaultSize通过比较最小的大小和MeasureSpec中的暂定大小,最终确定测量大小。

    view.png

    viewGroup的测量过程

    viewGroup中,首先会测量所有子view的大小,然后根据子view的大小来确定viewGroup的大小。由于不同的ViewGroup的测量结果不一样(LinearLayout的height是所有view的height之和,而FrameLayout的Height是所有子view的最大的height),所以viewGroup中没有实现具体的onMeasure();我们以FrameLayout来举例。
    首先还是measure()方法调用onMeaure方法,具体的测量实现我们看onMeasure()即可

    //这里的widthMeasureSpec、heightMeasureSpec
    //其实就是我们frameLayout可用的widthMeasureSpec 、
    //heightMeasureSpec
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      //1、获得frameLayout下childView的个数
      int count = getChildCount();
    //2、看这里的代码我们可以根据前面的Measure图来进行分析,因为只要parent
    //不是EXACTLY模式,以frameLayout为例,假设frameLayout本身还不是EXACTL模式,
     // 那么表示他的大小此时还是不确定的,从表得知,此时frameLayout的大小是根据
     //childView的最大值来设置的,这样就很好理解了,也就是childView测量好后还要再
    //测量一次,因为此时frameLayout的值已经可以算出来了,对于child为MATCH_PARENT
    //的,child的大小也就确定了,理解了这里,后面的代码就很 容易看懂了
      final boolean measureMatchParentChildren =
              MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
              MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
       //3、清理存储模式为MATCH_PARENT的child的队列
      mMatchParentChildren.clear();
      //4、下面三个值最终会用来设置frameLayout的大小
      int maxHeight = 0;
      int maxWidth = 0;
      int childState = 0;
      //5、开始便利frameLayout下的所有child
      for (int i = 0; i < count; i++) {
          final View child = getChildAt(i);
          //6、小发现哦,只要mMeasureAllChildren是true,就算child是GONE也会被测量哦,
          if (mMeasureAllChildren || child.getVisibility() != GONE) {
              //7、开始测量childView 
              measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
    
              //8、下面代码是获取child中的width 和height的最大值,后面用来重新设置frameLayout,有需要的话
              final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              maxWidth = Math.max(maxWidth,
                      child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
              maxHeight = Math.max(maxHeight,
                      child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
              childState = combineMeasuredStates(childState, child.getMeasuredState());
    
            //9、如果frameLayout不是EXACTLY,
              if (measureMatchParentChildren) {
                  if (lp.width == LayoutParams.MATCH_PARENT ||
                          lp.height == LayoutParams.MATCH_PARENT) {
    //10、存储LayoutParams.MATCH_PARENT的child,因为现在还不知道frameLayout大小,
    //也就无法设置child的大小,后面需重新测量
                      mMatchParentChildren.add(child);
                  }
              }
          }
      }
    
        ....
      //11、这里开始设置frameLayout的大小
      setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
              resolveSizeAndState(maxHeight, heightMeasureSpec,
                      childState << MEASURED_HEIGHT_STATE_SHIFT));
    
    //12、frameLayout大小确认了,我们就需要对宽或高为LayoutParams.MATCH_PARENTchild重新测量,设置大小
      count = mMatchParentChildren.size();
      if (count > 1) {
          for (int i = 0; i < count; i++) {
              final View child = mMatchParentChildren.get(i);
              final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
              final int childWidthMeasureSpec;
              if (lp.width == LayoutParams.MATCH_PARENT) {
                  final int width = Math.max(0, getMeasuredWidth()
                          - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                          - lp.leftMargin - lp.rightMargin);
    
      //13、注意这里,为child是EXACTLY类型的childWidthMeasureSpec,
      //也就是大小已经测量出来了不需要再测量了
      //通过MeasureSpec.makeMeasureSpec生成相应的MeasureSpec
                  childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                          width, MeasureSpec.EXACTLY);
              } else {
    
      //14、如果不是,说明此时的child的MeasureSpec是EXACTLY的,直接获取child的MeasureSpec,
                  childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                          getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                          lp.leftMargin + lp.rightMargin,
                          lp.width);
              }
    
      // 这里是对高做处理,与宽类似
              final int childHeightMeasureSpec;
              if (lp.height == LayoutParams.MATCH_PARENT) {
                  final int height = Math.max(0, getMeasuredHeight()
                          - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                          - lp.topMargin - lp.bottomMargin);
                  childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                          height, MeasureSpec.EXACTLY);
              } else {
                  childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                          getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                          lp.topMargin + lp.bottomMargin,
                          lp.height);
              }
    
      //最终,再次测量child
              child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
          }
      }
    }
    
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
    
            // 获取子视图的布局参数
            final LayoutParams lp = child.getLayoutParams();
    
            // 调用getChildMeasureSpec(),根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
             // getChildMeasureSpec()请回看上面的解析
             // 获取 ChildView 的 widthMeasureSpec
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            // 获取 ChildView 的 heightMeasureSpec
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            // 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    从上述代码我们可以将viewGroup的测量过程分为一下几个步骤

    1.初始化变量

    获取childCount,清理存储模式为MATCH_PARENT的child的队列,初始化最大宽度和最大高度。

    2.遍历所有子view,获取子view的MeasureSpec,测量子view的大小,获得所有子view的最大宽度和最大高度

    通过for循环遍历所有的子view,对于每一个子view,调用measureChildWithMargins,measureChildWithMargins中首先获得view的MeasureSpec,然后调用子view的measure完成子view的测量。

    3. 根据自身MeasureSpec和子view的最大宽度和最大高度,确定自身大小

    4. 重新确定LayoutParam==MATCH_PARENT的子view

    由于LayoutParam==MATCH_PARENT的子view的大小还没有确定,根据已经确定的Viewgroup的大小,重新计算子view的精确大小。

    最后viewGroup和view的测量过程结合起来画一张图

    viewGroup.png

    相关文章

      网友评论

        本文标题:安卓自定义view(二) - 测量

        本文链接:https://www.haomeiwen.com/subject/kmgayqtx.html