View的measure过程

作者: 吃葡萄皮不吐葡萄 | 来源:发表于2016-06-19 16:47 被阅读200次

    普通View的measure过程

    这里的普通View是指对应ViewGroup而言的。View的measure过程是由其measure()方法来完成的,measure()方法是一个final类型的方法。这意味着子类不能重写此方法。但是我们发现,View的measure()中会去调用View的onMeasure()方法,因此只需要看onMeasure的实现即可,View的onMeasure的实现如下所示。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
          setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));   
                //注解一
    }
    

    注解一:
    直接调用了setMeasuredDimension(int widthSize, int heightSize)方法,里面需要输入两个int形参,分别是得到的宽和高的测量尺寸。上面代码中,widthSize对应getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),heightSize对应着getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)。那么getDefaultSize()又是什么样的呢?

    public static int getDefaultSize(int size, int measureSpec){
          int result = size;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
    
          switch(specMode)
          case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
          case MeasureSpec.AT_MOST:
          case MeasureSpec.EXACTLY:
                result = specSize;
                break;
    }
    

    从上面的代码可以看出,getDefaultSize()这个方法逻辑很简单:

    • 如果该View的SpecMode是AT_MOST和EXACTLY两种情况,则getDefaultSize()返回的数值,也就是该View测量后的对应大小(宽或者是高),就是对应的MeasureSpec中的SpecSize。
    • 如果该View的SpecMode是UNSPECIFIED,这种情况一般是系统内部测量过程,此时,getDefaultSiez()方法会返回第一个参数size,以宽为例,即getSuggestedMinimumWidth()方法。根据源码,该方法会返回值的逻辑:如果该View没有设置背景,那么直接返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度两者中的最大值。

    上面的过程就是普通View的Measure过程,可以看出View的宽/高就是specSize决定的。同时,假如该View的布局属性是wrap_content,那么最后设置的测量后宽/高也为specSize。这种情况就和我们预想的不一样了。如果一个View的布局属性是wrap_content,那么它的测量宽/高应该是大于小于specSize,但是这是却直接设置为specSize。所以直接继承View的自定义控件需要重写onMeasure()方法,设置wrap_content情况下的自身大小,否则wrap_content属性就和match_parent属性相同了。重写的方式如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);             //先按照父类的方法计算,下面就行覆盖
          int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec):
          int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec):
          int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec):
          int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec):
    
          if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(mWidth,mHeight);                     //注解二
          } else if (widthSpecMode == AT_MOST) {
                setMeasureDimension(mWidth,heightSpecSize);
          } else if (heightSpecMode == AT_MOST){
                setMeasureDimension(widthSpecSize,mHeight);
          }
    }
    

    注解二:
    这里我们提前给View指定了一个默认的内部宽/高(mWidth,mHeight),并在wrap_content时设置此宽/高。同时,这里的mWidth和mHeight需要灵活指定,这样才和和wrap_content属性相符合。

    ViewGroup的measure过程

    ViewGroup除了需要完成自己的measure过程外,还需要遍历所有的子元素的measure方法,各个子元素去递归执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure()方法(在继承ViewGroup的类中,比如LinearLayout,一般都重写了这个方法),但是提供了一个measureChildren()的方法,如下:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec){
          final int size = mChildrenCount;
          final View[] children = mChildren;
          for(int i = 0; i < size; ++i){
                final View child = children[i];
                if ( (child.mViewFlags & VISIBILITY_MASK) != GONE){
                      measureChild(child, widthMeasureSpec,heightMeasureSpec);
                }
          }
    }
    

    从上面的代码可以看出,ViewGroup通过measureChildren()方法中for循环,对它所有的Visibility属性不等于GONE的子View执行measureChild()方法,measureChild()方法的实现如下:

    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasure){
          final LayoutParams lp = child.getLayoutParams();
          
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight,lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom,lp.height);
    
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    上面的代码的思路很清晰,首先通过getChildMeasureSpec()方法获得子View的MeasureSpec,然后调用子View的onMeasure()方法。通过上面的方法,就可以包装ViewGroup中所有的子View的会被测量(measure)。
    但是,ViewGroup并没有定义其测量的具体过程,即没有重写其父类View的measure()方法。这是因为ViewGroup是一个抽象类,不同的ViewGroup子类有不同的布局特性,所以需要这些子类自己去实现onMeasure()方法。不同的布局属性,实现方式不同。但是总体思路和View的onMeasure()方法是一样的,根据ViewGroup的不同SpecMode(UNSPECIFIED,AT_MOST,EXACTLY)来设置不同的测量值。

    补充

    上面已经对View的measure过程进行了详细的分析,现在考虑一种情况,比如我们想在Acitivity已启动的时候获取某个View的宽/高,这时候应该怎么做?如果直接在onCreate()或者onResume()方法里面去获取这个View的宽/高,是无法正确得到某个View的宽/高信息,这是因为View的measure过程和Activity的生命周期不是同步执行的,因此无法保证Activity执行了onCreate(),onStart(),onResume()时某个View已经测量完毕了,如果View没有测量完毕,那么获得的宽/高数据就是0。这里给出下面的四种方法:

    1. Activity中的onWindowFocusChanged()方法
      这个方法意味着:View已经初始化完毕了,宽/高都已经准备好了。但是,onWindowFocusChanged()方法会被调用多次,当Activity的窗口得到焦点或者失去焦点时均会被调用一次。具体来说,就是onResume()onPause()方法执行时,onWindowFocusChanged()方法就会被调用。典型代码如下:
    public void onWindowFocusChanged(boolean hasFocus){
          super.onWindowFocusChanged(hasFocus);
          if(hasFocus){
                int width = view.getMeasureWidth();
                int height = view.getMeasureHeight();
          }
    }
    
    1. 使用view.post(runnable)方法
      通过post方法可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。典型代码如下:
    protected void onStart(){
          super.onStart();
          view.post(new Runnable(){
                @override
                public void run(){
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();     
                }
          });
    }
    
    1. ViewTreeObserver
      使用ViewTreeOberser的众多回调可以完成这个功能。比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或View树内部的View的可见性发生改变时,OnGlobalLayoutListener接口中的onGlobalLayout()方法将被回调。但是,随着View树的状态改变等,onGlobalLayout()方法将被多次嗲用。典型代码如下:
    protected void onStart(){
          super.onStart();
          
          ViewTreeObserver observer = view.getViewTreeObserver();
          observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
                @SuppressWarnings("deprecation");
                @override
                public void onGlobalLayout(){
                      view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                }
          });
    }
    
    1. 通过view.measure(int widthMeasureSpec,int heightMeasureSpec)
      通过手动对View进行measure来得到宽/高,这种方法需要根据View的LayoutParams
    • match_parent 无法知道parentSize,所以这种情况无法measure出具体的宽/高
    • 具体数值:直接measure
    • wrap_content:MeasureSpec= (wrap_content) + 11111...1(最大值)

    相关文章

      网友评论

        本文标题:View的measure过程

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