美文网首页
View的测量过程

View的测量过程

作者: yk_looper | 来源:发表于2016-11-15 14:05 被阅读0次

    简述

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

    不管是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的测量过程

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