• View学习(二)-View的测量(measure)过程


    上一篇文章中,我们介绍了DecorViewMeasureSpec, 下面的文章就开始讨论View的三大流程。

    View的三大流程都是通过ViewRoot来完成的。ViewRoot对应于ViewRootImpl类,它是连接WindowManagerDecorView的纽带。在ActivityThread中,当Activity对象被创建完毕之后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

    View的绘制流程是从ViewRootImpl#performTraversals()开始的,它经过measure(测量),layout(布局),draw(绘制)三个过程才能最终将一个view显示出来。

    • measure过程测量View的宽高尺寸。Measure完成以后,就可以通过getMeasuredWidth()getMeasuredHeight()来获取View的宽高了。
    • layout过程确定View在父容器中的摆放位置。layout()完成之后,就可以通过getTop()getLeft(), getRight(), getBottom()来拿到View的左上角,右下角的坐标数据了。
    • draw过程负责将View绘制在屏幕上。draw()方法完成之后,View才能显示在屏幕上。

    ViewRootImpl#performTraversals()依次调用performMeasure, performLayout,performDraw三个方法。这三个方法依次完成View的 measure,layout,draw过程。

    借用网上的一张图,可以清晰的表达这个过程。

    • performMeasure调用 View#measure方法,而View#measure 则又调用 onMeasure方法,而对于View中的onMeasure方法,直接保存了测量得到的尺寸,而类似FrameLayout,RelativeLayout等各种容器ViewGroup,则在自己覆盖重写的的onMeasure方法中,对所有的子元素进行measure过程,此时measure流程就从父容器传递到子View中了,这就完成了一次measure过程,最终这个递归的过程完成了整个View树的遍历。
    • performLayout,performDraw的传递流程也是类似的。唯一不同的是performDraw的传递过程是通过draw方法中通过dispatchDraw来实现的,但是道理都是相同的。
    • traversals的意思就是遍历。

    其实到了这个时候,我们大概的流程就已经知道了,那么剩下的具体就看看View和ViewGroup当中的具体的测量过程了。这两个还要分情况讨论,因为View是属于自己一人吃饱,全家不饿,把自己测量好就行了,可是ViewGroup不仅要测量自己,还要遍历去调用所有子元素的measure过程,从而形成一个递归过程。

    而measure的大流程则如下:

    测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有孙控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程有可能重复执行多次,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。

    借用某个大神博客上的一张图

    View的measure过程

    View的测量过程由measure()来完成,measure()方法是final的,子类无法重写覆盖。它会调用onMeasure方法。

    根据measure的源码,View其实也是比较喜欢偷懒的。意思是执行了measure方法并不一定将测量过程完整走一遍(就是调用onMeasure方法)。具体来说,如果View发现不是强制测量,且本次传入的MeasureSpec与上次传入的相同,那么View就没必要重新测量一遍。如果真的需要测量,View也先查看之前是否缓存过相应的计算结果,如果有缓存,直接保存计算结果,就不用再调用onMeasure了。这样也是最大限度的提高效率,避免重复工作。

    onMeasure方法代码如下:

    //View#onMeasure
    /**
     *  Measure the view and its content to determine the measured width and the
     *  measured height. This method is invoked by {@link #measure(int, int)} and
     *  should be overridden by subclasses to provide accurate and efficient
     *  measurement of their contents.
     */
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    首先需要注意,该方法注释的一句话,

    This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.

    这就意味着,我们在进行自定义View时,是应该重写覆盖这个方法的。

    先看一下onMeasure方法中调用的getDefaultSize方法。

    //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;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    

    UNSPECIFIED模式一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽度为getSuggestedMinimumWidth()返回的值,而getSuggestedMinimumWidth()可以用一句伪代码来表示。

        mMinWidth = android:minWidth;
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    

    对于测量过程而言,width,height过程都是一样的流程。所以为了行文简单,所以我们就简单的只说width。

    再回过头来看 getDefaultSize方法的源码。可以看到,不管我们的View的LayoutParams设置的是 match_parent或者 wrap_content,它的最终尺寸都是 相同的。再结合上一篇文章末尾我们根据getChildMeasureSpec方法而整理出来的那张表。

    当View为wrap_content时,它的size就是parentSize-padding,这和match_parent时,size是一样的。(虽然mode可能不一样)。

    结论就是: 对于View来讲,使用wrap_content和使用match_parent效果是一样的,它俩的默认size是相同的。

    所以各个子View,(当然也包括我们extends View,自定义的View),在测量阶段,针对wrap_content都要做相应的处理,否则使用 wrap_content就和使用 match_parent效果都是一样的, 那就是默认填充满父View剩下的空间。

    而我们在自定义View时,针对wrap_content情况一般处理方式是,在onMeasure中,增加对MeasureSpec.AT_MOST的判断语句,结合具体业务场景或情况,设定一个默认值,或计算出一个值。

    wrap_content的意思就是 包裹内容,但是仔细思考一下,内容又是什么呢,具体到不同的子View场景,肯定有不同的意义,所以从这个角度来思考,作为继承结构上最顶级,同时也是最抽象的View而言,wrap_cotentmatch_parent默认尺寸一样,也就有道理了。

    其实普通View的onMeasure逻辑大同小异,基本都是测量自身内容和背景,然后根据父View传递过来的MeasureSpec进行最终的大小判定,例如TextView会根据文字的长度,大小,行高,行宽,显示方式,背景图片,以及父View传递过来的模式和大小最终确定自身的大小.

    在测量结束时,调用了setMeasuredDimension来存储测量得到的宽高值,该方法源码当中,注释是这样的。

    This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.

    如果我们自定义View时,重写了onMeasure方法,那么就需要调用setMeasuredDimension方法来保存结果,否则就会抛出异常。

    ViewGroup的measure过程

    ViewGroup比View复杂,因为它要遍历去调用所有子元素的measure过程,各个子元素再递归去执行这个过程。正所谓领导都要能者多劳之,领导要干的工作也比较多。

    ViewGroup是一个抽象类,它没有重写View的onMeasure方法,这是合理的,因为各个不同的子容器(比如LinearLayout, FrameLayout,RelativeLayout等)它们有它们特定的具体布局方式(比如如何摆放子View),所以ViewGroup没办法具体统一,onMeasure的实现逻辑,都是在各个具体容器类中实现的。

    但是ViewGroup当中,提供了三个测量子控件的方法。

    /**
      *遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
      */
     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);
            }
        }
    }
    
    /**
    * 测量某一个child的宽高
    */
    //ViewGroup中方法。
    protected void measureChild (View child, int parentWidthMeasureSpec,
           int parentHeightMeasureSpec) {
       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);
    }
    
    /**
    * 测量某一个child的宽高,考虑margin值
    */
    protected void measureChildWithMargins (View child,
           int parentWidthMeasureSpec, int widthUsed,
           int parentHeightMeasureSpec, int heightUsed) {
       final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
       //获取子控件的宽高约束规则,相比于 measureChild方法,这里考虑了 lp.margin值
       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);
    }
    

    望名知意,根据方法的命名我们就能知道每个方法的作用。measureChild,或measureChildWithMargin,它们的作用就是取出child的layoutParams,然后再通过getChildMeasureSpec来创建child的MeasureSpec,然后再将MeasureSpec传递给child进行measure。这样就完成了一轮递归。

    所以我们在上一篇博客中,着重介绍了getChildMeasureSpec方法,指出这个方法是很重要的。

    • measureChildWithMarginmeasureChild的区别就是父控件是否支持margin属性。

    因为各个不同的容器(比如LinearLayout,FrameLayout,RelativeLayout等),都有各自的measure方式,所以我们就挑选LinearLayout来看一下它的measure流程。

        //LinearLayout
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (mOrientation == VERTICAL) {
                measureVertical(widthMeasureSpec, heightMeasureSpec);
            } else {
                measureHorizontal(widthMeasureSpec, heightMeasureSpec);
            }
        }
    

    我们就选择竖直方向的measure过程来分析。
    源码比较长,我们只看主流程。

    //LinearLayout#measureVertical
    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
            
            // ....
    
            // Determine how big this child would like to be. If this or
            // previous children have given a weight, then we allow it to
            // use all available space (and we will shrink things later
            // if needed).
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
    
            final int childHeight = child.getMeasuredHeight();
            
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
    
    }
    

    系统会遍历子元素,并对每个子元素执行measureChildBeforeLayout方法,该方法内部会调用子元素的measure方法,这样各个子元素就依次进入measure过程。系统通过mTotalLength这个变量动态存储LinearLayout在竖直方向上的高度,并且伴随着每测量一个子元素,mTotalLength则会逐步增加。增加的部分包括了子元素的高度以及子元素在竖直方向上的margin等。

    而当所有子元素都测量完毕之后,LinearLayout则会测量自己的大小。

     //LinearLayout#measureVertical
     // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    
    int heightSize = mTotalLength;
    
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
    // Reconcile our calculated size with the heightMeasureSpec
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    
    
    
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
    

    针对竖直方向的LinearLayout而言,它在水平方向上的测量过程遵循View的测量过程,而竖直方向则和View有所不同,具体来说,如果它的高度参数是具体数值或match_parent,则测量过程和View一致,即高度为SpecSize;而如果高度参数采用的是wrap_content,那么它的高度就是所有子元素所占用的高度总和,但是仍然不能超过它父容器的剩余空间。并且最终高度还要考虑竖直方向的padding。

    具体可以参考以下源码:

      //LinearLayout
      public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
            final int specMode = MeasureSpec.getMode(measureSpec);
            final int specSize = MeasureSpec.getSize(measureSpec);
            final int result;
            switch (specMode) {
                case MeasureSpec.AT_MOST:
                    if (specSize < size) {
                      //当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,
                    //我们就会用掩码MEASURED_STATE_TOO_SMALL向测量结果加入尺寸太小的标记
                    //这样其父ViewGroup就可以通过该标记知道其给子View的尺寸太小了,
                    //然后可能分配更大一点的尺寸给子View
                        result = specSize | MEASURED_STATE_TOO_SMALL;
                    } else {
                        result = size;
                    }
                    break;
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
                case MeasureSpec.UNSPECIFIED:
                default:
                    result = size;
            }
            return result | (childMeasuredState & MEASURED_STATE_MASK);
        }
    

    resolveSizeAndState方法和getDefaultSize方法类似,其内部实现逻辑是一样的,不过区别在于,resolveSizeAndState方法除了返回尺寸信息,还会返回测量的status标志位信息。

    View的测量过程是三大流程中比较复杂的,只有测量完毕之后,我们才有可能得到正确的宽高值。

    而View的measure过程和Activity的生命周期方法是不同步的。所以我们不能简单的通过onStart,onResume方法来获取View的尺寸值。

    参考内容:


    作者: www.yaoxiaowen.com

    github: https://github.com/yaowen369

    欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系

    <p >
    		 欢迎转载,转载请注明出处.谢谢
    </p>
    
    
    <script type="text/javascript">
     function    Curgo()   
     {   
         window.open(window.location.href);
     }   
    </script>
    
  • 相关阅读:
    修改带!important的css样式
    解决Eclipse导入项目是提示错误:Some projects cannot be imported because they already exist in the workspace
    HTML5——canvas:使用画布绘制卡片
    vue:更改组件样式
    导入导出大量excel文件
    winfrom控件Treeview绑定数据库中的资料(节点控件)
    Winfrom将excel中的数据导入sqlserver数据库中的方法
    C# 将DataTable表中的数据批量插入到数据库表中的方法
    创建Sql数据表的sql代码
    Winfrom之SplitContainer控件
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/7056763.html
Copyright © 2020-2023  润新知