View的测量过程

简述

android的界面显示其实分成两块,一块了系统的DecorView(顶级view)和普通view ,而DecorView包含一个竖直的LinearLayout,上部分是titleBar ,下部分是一个id为content的frameLayout,不管是显示过程还是事件分发,都是由它传递而来。另一块就是我们自己设置填充到DecorView中framelayout的view 。现在是不是有点体会了,为什么在给activity设置布局的时候方法是setContentView() ,因为我们的确是把view设置到id为content的FrameLayout当中的。

View的测量过程_第1张图片

不管是DecorView或者是普通view ,要显示到界面上都要经历三个过程:measure(测量宽高)layout(布局位置)draw(绘制)


view的测量过程

View的测量过程决定了它的宽高,MeasureSpec参与了view的测量,所以我们有必要详细了解一下

MeasureSpec

MeasureSpec代表了一个32位的int值,高2位是SpecMode(测量模式),低30位是测量SpecSize(测量尺寸),而SpecMode又分为三种,不同的模式下最终生成的尺寸是不一样的

  • AT_MOST (最大模式):父容器指定一个可用的大小即SpecSize,子view的大小不可超过该尺寸,具体视图子view的实现而定,对应于wrap_content属性

  • EXACTLY (精准模式):父容器已经计算出了确定的值,子view的最终大小就是SpecSize的值,对应于match_parent和确定值

  • UNSPECIFIED (未指定):父容器不对子view做任何限制,需要多大就给多大,一般用于系统内部,我们就不用太过关心了


现在我们大概了解了什么是MeasureSpec,那么它是怎样生成的呢?

对于DecorView,它的MeasureSpec由屏幕尺寸和自身的LayoutParamas决定。对于普通view,它的measure过程由ViewGroup的measureChild()调用

protected void measureChild(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);}

由此可见,子view的measureSpec由父容器的MeasureSpec、padding/margin、自身layoutParamas决定,具体实现由于代码稍多就不贴出来了,有兴趣可以自己研究。总结起来就是:

  • 子view的宽高是具体值,SpecMode总是EXACTLY,最后的宽高就是设置的值

  • 子view的宽高为match_parent,则SpecMode(测量模式)由父容器决定,最后的宽高取决于父容器的测量尺寸

  • 子view的宽高为wrap_content,则SpecMode(测量模式)始终是AT_MOST,最后的宽高不能超过父容器的剩余空间

View的measure过程

view的measure()方法是一个final方法,里面调用的是onMeasure(),如下:

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

setMeasureDimension()会把view的宽高测量设置进系统,再来看看系统默认的处理测量的宽高的方法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;
    //在我们关心的这2中模式下,返回的其实就是测量值
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这个方法的逻辑也很简单,简单来说就是直接返回了测量值(SpecSize),当然这是系统默认的处理,我们在自定义view的时候可以重写onMeasure()方法,根据自己的逻辑把测量值设置进去。

额外补充一点,如果我们自定义view的时候默认系统设置测量值的方法,那么wrap_content的作用效果跟match_parent一样,为什么?回看一下上面的总结,当view的宽高属性值为wrap_content时,测量模式为AT_MOST,测量尺寸为specSize,就是父容器的剩余可用尺寸,这不就跟match_parent效果一样了么!

那有什么方法能处理么?很简单,重写onMeasure(),当宽高属性为wrap_content的时候,给设置一个默认的大小,ok搞定

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取宽高的测量模式
    int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //当宽/高为wrap_content即对用AT_MOST的时候,设置一个默认值给系统    
    if (withSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(defWith,defHeight);
    }else if (withSpecMode==MeasureSpec.AT_MOST){ 
       setMeasuredDimension(defWith,heightSpecSize);
    }else {
        setMeasuredDimension(withSpecSize,defHeight);
    }
}

ViewGroup的measure过程

对于ViewGroup本身,系统并没有提供一个默认的onMeasure方法,因为不同特性的ViewGroup(比如LinearLayoutRelativeLayout)他们的测量方式肯定是不一样的,所以需要子类自己去实现。而对于ViewGroup里面的view,则会通过measureChildren()循环遍历每一个view,然后调用measureChild()(这个方法的逻辑跟measureChildWithMargins()一模一样),如此从而完成测量。

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);
        }
    }
}

measureChild()方法的核心是:取出子view的layoutParamas和自身的MeasureSpec生成MeasureSpec传递给子view的measure()方法,完成对子view的测量

获取View的测量宽/高

measure()方法完成后,可以通过getMeasureHeight/with获取测量的宽高,但是这时取出的值并不一定是最终的值,某些情况下系统会多次调用measure才能完成测量。所以最准确的方式是在layout中获取测量的宽高值


如果我们想在activity或者fragment中获取一个view的测量宽高怎么办?

  • 在activity中可在onWindowFocusChanged()方法里面获取,缺点就是activity得到或者是去焦点时都会回调,可用导致频繁的调用
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus){
        etUsername.getMeasuredWidth();
    }
}
  • view.post(runnable)这种方法是极力推荐的,投递一个runnable到消息队列,当looper处理这条消息的时候,view也初始化好了
etUsername.post(new Runnable() {
    @Override    public void run() {
        etUsername.getMeasuredWidth();
    }
});

你可能感兴趣的:(View的测量过程)