美文网首页
View工作原理之measure

View工作原理之measure

作者: 李die喋 | 来源:发表于2019-07-29 21:22 被阅读0次

    MeasureSpec

    基本概念

    MeasureSpec参与了View的measure过程,系统根据MeasureSpec来测量出View的测量宽/高

    • 对于DecorView,其MeasureSpec是由窗口尺寸的大小和自身的LayoutParams来共同确定的
    • 对于普通的View,是由父容器的MeasureSpec和自身的LayoutParams共同决定的

    一个MeasureSpec由MeasureMode和MeasureSize组成,分别指测量模式和规格大小。

    public static class MeasureSpec {
        //偏移量
        private static final int MODE_SHIFT = 30;
        //1100 0000 0000 0000 0000 0000 0000 0000
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        
        //合并SpecSize和SpecMode生成MeasureSpec
        public static int makeMeasureSpec(int size,int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        
        public static int getMode(int measureSpec) {
                //noinspection ResourceType
                return (measureSpec & MODE_MASK);
        }
        
        public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
        }
    }
    

    从上面的源码可以看到MeasureSpec是一个32位int值,高两位代表SpecMode,低30位代表SpecSize。SpecMode有三种模式:

    • UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
    • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。他对应于LayoutParams中的match_parent和具体的数值这两种情况。
    • AT_MOST:父容器指定了一个可用大小,View的大小不能超过父容器的SpecSize。具体的值要看不同View的具体实现,它对应于LayoutParams中的wrap_content。

    DecorView的MeasureSpec创建过程

    在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    其中传入的参数desiredWindowWidth、desiredWindowHeight是屏幕的宽度和高度,接着再看下getRootMeasureSpec方法的实现:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    根据LayoutParams中的宽/高的参数来划分:

    • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
    • LayoutParams.WTAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
    • 固定大小:精确模式,大小为LayoutParams中指定的大小。

    普通View的MeasureSpec创建过程

    这里的View指的是我们布局中的View,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:

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

    从上面的代码看出,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关。得到子元素MeasureSpec的具体情况可以看下getChildMeasureSpec()方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //获取子view的真实宽度
        int size = Math.max(0, specSize - padding);
    
        int resultSize = 0;
        int resultMode = 0;
    
        switch (specMode) {
        // 若当前view的SpecMode是EXACTLY
        case MeasureSpec.EXACTLY:
            //宽/高是固定的数据
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
               //如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
                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;
        }
        //合并得到MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    上述代码主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。

    child布局参数/父元素Mode EXACTLY AT_MOST USPECIFIED
    dp\px EXACTLY
    childSize
    EXACTLY
    childSize
    EXACTLY
    childSize
    match_parent EXACTLY
    parentSize
    AT_MOST
    parentSize
    UNSPECIFIED
    0
    wrap_content AT_MOST
    parentSize
    AT_MOST
    parentSize
    UNSPECIFIED
    0

    总结

    • Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象与DecorView创建联系
    • View的绘制流程从ViewRootImpl的PerformTraversals()方法开始。performTraversals()先计算出窗口的大小,再通过getRootMeasure()方法,计算出DecorView的宽高,传入performMeasure()方法中
    private void performTraversals() {
      ...
      if (!mStopped) {
        boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
        if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
            
            // 获取测量规格,mWidth 和 mHeight 当前视图 frame 的大小
            // lp是WindowManager.LayoutParams
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    
             // Ask host how big it wants to be
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
          }
          ...
        }
    }
    
    • 开始view的measure,起作用的其实是onMeasure()方法

    View的measure过程

    View的measure方法中回去调用View的onMeasure方法,如下所示:

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

    setMeasuredDimension()会设置View宽高的测量值

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

    当AT_MOST和EXACTLY这两种情况时,返回的值都是measureSpec的specSize,specSize是测量后的大小。

    UNSPECIFIED这种情况,一般用于系统内部的测量,在这种情况下,View的大小为getDefaultSize的第一个参数size,宽高分别为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
     protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
    

    这里判断了View是否设置了背景,若设置了背景,View的宽度为mMinWidth。mMinWidth对应于android:minWidth这个属性所指的值,若不设置默认为0。如果View设置了背景,则View宽度为max(mMinHeight, mBackground.getMinimumHeight()):

    public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }
    //返回drawable内在高度,如果drawable没有内在高度返回-1
    public int getIntrinsicHeight() {
            return -1;
    }
    

    根据上述代码,得到getMinimumHeight()返回的就是drawable的原始高度,前提是drawable有原始高度,没有就返回0。例如:ShapeDrawable无原始宽/高,BitmapDrawable有原始宽/高。

    wrap_content和自定义问题

    从getDefaultSize()方法来看,若按照onMeasure的默认方法实现,当宽/高设置成wrap_content或match_parent时,最后的结果都是specSize,也就是两种情况下都是match_parent的效果。所以在自定义view下的时候需要对wrap_content进行处理。

    在自定义view的时候,我们通过判定宽高是否是wrap_content来给宽/高是wrap_content的情况设定一个默认的内部宽/高。像TextView、ImageView都对这里做了处理。

    View的Measure过程总结

    • 从ViewGroup#measureChildWidthMargins开始,计算View的MeasureSpec
    • measureChildWidthMargins()调用child.measure()开始测量过程
    • View#measure()调用onMeasure()
    • onMeasure()方法测量View宽高
      • 根据是否有背景图来确定View的默认大小
      • 根据SpecMode来确定View测量后的大小是SpecSize还是默认大小
    • onMeasure()测量后的数据给全局变量mMeasuredWidth和mMeasuredHeight

    ViewGroup的measure过程

    对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个过程。

    ViewGroup#measureChildren

    遍历子view调用measureChild()

    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];
            //当child不是GONE时,就去测量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
    

    ViewGroup#measureChild()

    • 前面提到过的measureChildWithMargins()中的getChildMeasureSpec(),开始测量
    • 调用子view向下递归测量
    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);
    }
    

    ViewGroup没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。

    获得View宽高的四种方法

    在实际操作中我们可能回去获取某个view的宽和高,但是在onCreate() onResume()方法中一般是获取不到的,因为View的measure过程和Activity生命周期方法不是同步执行的。

    1.Activity/View#onWindowFocusChanged

    onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了。当Activity窗口得到焦点和失去焦点是均会被调用一次,当频繁进行onPause和onPause,会被频繁调用。

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
    

    2.view.post(runnable)

    通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
    

    3.ViewTreeObserver

    可以通过ViewTreeObserver添加监听来监听View的各种动态。onGlobalLayout()会在View树发生改变或是View树内部的View的可见性发生改变时调用。

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
    

    4.手动measure(view.measure(int widthMeasureSpec,int heightMeasureSpec))

    对view进行measure得到view的宽 高,需要根据view的LayoutParams来分:

    match_parent

    无法measure出具体宽高。因为要测量出当前view尺寸,需要知道父容器的属于空间,无法知道parentSize。

    wrap_content

    ((1 << 30) - 1)是specMode支持的最大值

    int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
    int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
    view.measure(widthSpec, heightSpec);
    

    具体数值

    LayoutParams lp = view.getLayoutParams();
    int widthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
    int heightSpec = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
    view.measure(widthSpec, heightSpec);
    

    相关文章

      网友评论

          本文标题:View工作原理之measure

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