Android之自定义View的死亡三部曲之(Measure)

文章独家授权公众号:码个蛋
更多分享:http://www.cherylgood.cn

我们在上一篇Android之View的诞生之谜分析了从Activity的创建到View开始执行测量、布局、绘制之前所经历的一些事情以及处理状态栏的一些小技巧等,如果你也想知道的话,不妨点击一下-Android之View的诞生之谜哦,或许你面有你想要的呢

死亡三部曲第一部(Measure)->我只想知道你的三围是多少

  • 我们在上一章节Android之View的诞生之谜中分析了系统从启动actiivty到调用setContentView加载我们的xml布局文件,但是此时我们的View是不可见的,因为我们还没有对其进行如下操作:
    1、测量:我还不知道你的三围呢(你要占多少屏幕),我怎么能轻易让你出场呢----测量工作
    2、布局:你把三围给我了,但是你还没告诉我你要站在那里,对位置的分布有什么要求----行布局操作
    3、绘制:好,现在我要给你花点妆,美美地出场----绘制操作

  • OK,我们在上篇中分析道,系统加载好布局资源之后,会触发ViewRootImpl的performTraversals方法,在该方法内部会开始执行测量、布局、绘制的工作,也就是我们的死亡三部曲的开始。

  • 我们来看ViewRootImpl的performTraversals方法的源码,为了简洁,我只留下关键的代码。

    private void performTraversals() {
          ...
      if (!mStopped) {
        //1、获取顶层布局的childWidthMeasureSpec
          int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        //2、获取顶层布局的childHeightMeasureSpec
          int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
          //3、测量开始测量
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
          }
      } 
    
      if (didLayout) {
        //4、执行布局方法
          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
          ...
      }
      if (!cancelDraw && !newSurface) {
       ...
        //5、开始绘制了哦
              performDraw();
          }
      } 
      ...
    }
    
    
  • 可以看到,里面按顺序调用了performMeasure、performLayout、performDraw三个方法,也就是对应的测量、布局、绘制,再继续深入之前,我们需要先补充点能量,对MeasureSpec已了解的同学可以跳过下面一段。


能量站启动。。。。。。

1、MeasureSpec
  • MeasureSpec 是个什么东西呢?其实MeasureSpec是View内部的一个静态类,在编写测量控件的代码中一定能见到其美丽的身影,他的诞生是那么的无私->为何辅助view的测量能够更好的进行。

  • 我们可以先从官方文档中初步了解一下:

    • A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode. There are three possible modes:
    • MeasureSpec对象中封装了从父对象传递给孩子的布局所需数数据(你要成为我的子控件,你要在我里面占位置,你先要知道我有多少空间吧?)。每一个MeasureSpec对象包含了对于宽度和高度的描述(也就是父控件告诉子控件,我有多大点地和我对于空间的使用策略等)。 MeasureSpec由大小和模式组成。有三种可能的模式:
    • 1、UNSPECIFIED 父控件还不知道子控件的大小,对子控件也没有任何约束,说你想占多少地方就占吧。(这个一般很少用到)
    • 2、EXACTLY 这种状态下的控件的大小是明确的。
    • 3、AT_MOST 父控件对子控件说,我还不知道你的大小,我给你自由,我的地方是这么大,你按你的意愿来,但最大也只能跟我一样大了,注意哦,可能需要二次测量,后面会讲到。
  • 为了更好的理解三种模式,我们可以看一下实际测量的源码里是如何处理的

  • 呃我想想,好吧,我们从ViewGroup.measureChild方法入手吧,这个是viewGroup测量下面的childView的方法,看源码,解释我就直接写源码里了,便于阅读:

    // 从参数我们能得到一些信息 第一个参数是child,
    // 也就是我们要测量的子view ,第二、第四个参
    // 数分别为父view的MeasureSpec,第三个第五个
    // 分别表示parentView的宽和高已经被使用了的大小,
    //从参数上我们可以猜测,子view的测量结果与父
    //View的MeasureSpec是息息相关的
    protected void measureChildWithMargins(View child,
          int parentWidthMeasureSpec, int widthUsed,
          int parentHeightMeasureSpec, int heightUsed)  {
    
    //1、获取子View的layout参数,因为子View的大小也跟布
    //局参数相关哦,这种view很气人,他要跟别人产生一定的距离
      final MarginLayoutParams lp = (MarginLayoutParams)
       child.getLayoutParams();
    
    //2、测量childView的宽的MeasureSpec,第一个参数会
    //传入parent的 MeasureSpec,第二个参数经过计算后实际
    //得到的是parent已被使用的宽度和child的padding和margin
    //消耗的宽度,第三个参数为child的的大小,这个大小并
    //不一定是child最后的大小哦,只能说是我们希望创建的大小
    // 例如在xml文件中的layout_width指定的值
      final int childWidthMeasureSpec = getChildMeasureSpec
      (parentWidthMeasureSpec,mPaddingLeft + mPaddingRight
       + lp.leftMargin + lp.rightMargin
                      + widthUsed, lp.width);
    
    //3、测量childView的高的MeasureSpec,参数与测量宽类似
    //这里就不多说了
      final int childHeightMeasureSpec = getChildMeasureSpec(
      parentHeightMeasureSpec,mPaddingTop 
      + mPaddingBottom + lp.topMargin + lp.bottomMargin
                      + heightUsed, lp.height);
    
    //4、获得childview的高、宽的MeasureSpec后,就可以
    //确定child的大小了
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    

  • 上面的代码经过分析就很好理解了,我们继续看getChildMeasureSpec方法的源码,看里面是怎么测量出child的宽、高的MeasureSpec的呢?源码不多,一百多行,我们一起来看下

    //从上面我们知道spec 是parent的MeasureSpec,padding是
    //已被使用的大小,childDimension为child的大小
    public static int getChildMeasureSpec(
    int spec, int padding, int childDimension) {
    
    //1、获取parent的specMode
      int specMode = MeasureSpec.getMode(spec);
    
    //2、获取parent的specSize
      int specSize = MeasureSpec.getSize(spec);
    //3、size=剩余的可用大小
      int size = Math.max(0, specSize - padding);
    
      int resultSize = 0;
      int resultMode = 0;
    
      //4、通过switch语句判断parent的集中mode,分别处理
      switch (specMode) {
      // 5、parent为MeasureSpec.EXACTLY时
      case MeasureSpec.EXACTLY:
    
          if (childDimension >= 0) {
        //5.1、当childDimension大于0时,表示child的大小是
            //明确指出的,如layout_width= "100dp";
              // 此时child的大小= childDimension,
              resultSize = childDimension;
    
              //child的测量模式= MeasureSpec.EXACTLY
              resultMode = MeasureSpec.EXACTLY;
    
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
    
        //5.2、此时为LayoutParams.MATCH_PARENT
        //也就是    android:layout_width="match_parent"
          //因为parent的大小是明确的,child要匹配parent的大小
          //那么我们就直接让child=parent的大小就好
              resultSize = size;
    
            //同样,child的测量模式= MeasureSpec.EXACTLY
              resultMode = MeasureSpec.EXACTLY;
    
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
      //5.3、此时为LayoutParams.WRAP_CONTENT
        //也就是   android:layout_width="wrap_content"  
        // 这个模式需要特别对待,child说我要的大小刚好够放
        //需要展示的内容就好,而此时我们并不知道child的内容
        //需要多大的地方,暂时先把parent的size给他
    
              resultSize = size;
          //自然,child的mode就是MeasureSpec.AT_MOST的了
              resultMode = MeasureSpec.AT_MOST;
          }
          break;
    
      // 5、parent为AT_MOST,此时child最大不能超过parent
      case MeasureSpec.AT_MOST:
          if (childDimension >= 0) {
              //同样child大小明确时,
              //大小直接时指定的childDimension
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // child要跟parent一样大,resultSize=可用大小
              resultSize = size;
            //因为parent是AT_MOST,child的大小也还是未定的,
            //所以也是MeasureSpec.AT_MOST
              resultMode = MeasureSpec.AT_MOST;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              //又是特殊情况,先给child可用的大小
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;
    
      // 这种模式是很少用的,我们也看下吧
      case MeasureSpec.UNSPECIFIED:
          if (childDimension >= 0) {
              // 与前面同样的处理
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size... find out how big it should
              // be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size.... find out how
              // big it should be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          }
          break;
      }
      //通过传入resultSize和resultMode生成一个MeasureSpec.返回
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    
  • 小结:从上面我们了解的MeasureSpec是用来辅助测量view的大小的一个辅助类,我们分析的MeasureSpec的mode和size是根据parent和child相互决定的。下面是我网上收集的一个MeasureSpec图片

  • 能量补充完毕,我们继续回到开头的ViewRootImpl.performMeasure源码上分析,在1、2两步我们获得了DecorView的MeasureSpec,然后通过传入MeasureSpec开始了我们的测量之旅。那么我们继续看3里面是如何测量的。

    private void performMeasure(int childWidthMeasureSpec, 
      int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
      //1、mView其实就是我们的顶层DecorView,从DecorView开始测量
      mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
      Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    }
    
    
  • 补充:在Android Touch事件分发机制详解之由点击引发的战争我们分析过DecorView实际是集成自FrameLayout,那么我们看frameLayout,发现frameLayout并没有measure方法,但是它又继承自ViewGroup。所以肯定是ViewGroup了,然而,ViewGroup也没找到measure方法,那么继续查看其parent 类View,哈哈,在view中被我找到了吧,我们看代码。只保留了关键的一句,不要打我。

    public final void measure(int widthMeasureSpec,
     int  heightMeasureSpec) {
    ...
    
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
    }
    
    
  • 从上面我们看到,里面调用了onMeasure方法,这里要注意了:

    • 1、我们的ViewGroup并没有重写View的onMeasure方法,而但是我们android开发中的四大布局 FrameLayout、LinearLayout、RelativeLayout、AbsoluteLayout都是通过继承ViewGroup来实现的,而且里面也重写onMeasure方法。
    • 2、所以我们可以分两种情况来看待:1、布局类控件;2、一般展示类控件;
    • 3、自定义控件过程中,一般情况下我们也需要通过重写onMeasure来做一些特殊处理。

  • 接下来我们可以从两个方向去分析onMeasure方法:
    1、View.onMeasure
    2、布局类的,例如. FrameLayout.onMeasure

  • 那么我们先从View.onMeasure吧,毕竟他才是最原始的。

  • View.onMeasure源码如下,虽然就几句,但是做的事情可不少哦!

     protected void onMeasure(int widthMeasureSpec, 
    int heightMeasureSpec) {
     setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(),
                                    widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(),
                                     heightMeasureSpec));
    }
    
    
  • 1、调用setMeasuredDimension设置view的大小

  • 2、调用getDefaultSize获取View的大小,

  • 3、getSuggestedMinimumWidth获取一个建议最小值

  • 调用顺序为onMeasure-> setMeasuredDimension-> getDefaultSize-> getSuggestedMinimumWidth

  • 我们逆过来分析一下,首先getSuggestedMinimumWidth这个是什么呢?我们点进源码看一下:

    protected int getSuggestedMinimumWidth() {
      return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
    
    
  • 里面代码很少,判断是否有背景,没有的话返回mMinWidth,这个mMinWidth其实就是android:minWidth=""属性设置的值。也就是假设没设置有背景的情况下,就以设置minWidth值为准

  • 如果设置有背景,那么就去背景的实际宽度与minWidth中大的一个。

  • getMinimumWidth()可以理解成背景的bitmap形式下的实际宽度值。

  • 然后我们看getDefaultSize这个方法,这是一个静态工具方法,他返回的是view的大小:

     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;
    }
    
    
  • 第3点很重要,你有没有发现,AT_MOST与EXACTLY模式下,返回的值居然是一样的,那岂不是wrap_content与match_parent是等效的?不要打我,我可没骗你哦

  • 那么,我们实际开发中肯定要处理这个情况,所以我们在自定义直接继承View来实现的控件时,一定要自己处理这两种情况哦。否则wrap_content属性是等效于match_parent的哦

  • 之后就到我们的setMeasuredDimension方法了,前面说了,setMeasuredDimension是设置view的大小的。我们进去看一下源码

    protected final void setMeasuredDimension(int   measuredWidth, int measuredHeight) {
        //1、判断是否使用视觉边界布局
      boolean optical = isLayoutModeOptical(this);
      //2、判断view和parentView使用的视觉边界布局是否一致
      if (optical != isLayoutModeOptical(mParent)) {
          //不一致时要做一些边界的处理
          Insets insets = getOpticalInsets();
          int opticalWidth  = insets.left + insets.right;
          int opticalHeight = insets.top  + insets.bottom;
    
          measuredWidth  += optical ? opticalWidth  : -opticalWidth;
          measuredHeight += optical ? opticalHeight : -opticalHeight;
      }
      //3、重点来了,经过过滤之后调用了setMeasuredDimensionRaw方法,看来应该是这个方法设置我们的view的大小
      setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    
    
  • 我们继续看setMeasuredDimensionRaw方法

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
      //最终将测量好的大小存储到mMeasuredWidth和mMeasuredHeight上,所以在测量之后我们可以通过调用getMeasuredWidth获得测量的宽、getMeasuredHeight获得高
      mMeasuredWidth = measuredWidth;
      mMeasuredHeight = measuredHeight;
    
      mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    
    

小结:

  • 测量view的顺序为measure->onMeasure-> setMeasuredDimension-> setMeasuredDimensionRaw,由setMeasuredDimensionRaw最终保存测量的数据。
  • 以上是测量一个view的过程,这样子我们的view的测量工作就结束了。

  • 接下来我们来看下布局类frameLayout是如何测量的,我们同样看FrameLayout的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);
          }
      }
    }
    
    
  • 至此,View的三围已经测出来了,本篇略长,测量在android的死亡三部曲中是第一部,也是里面最复杂、重要的一部,快看下你的三围是多少吧!


总结:

  • View的测量,重点是抓住MeasureSpec在其中体现的作用,MeasureSpec贯穿了View测量的整个过程,明白其的作用,也就明白了View测量的一半知识了。
  • View的Layout将在下一章进行分析

推荐阅读更多精彩内容