美文网首页
Android View的工作流程 - measure过程

Android View的工作流程 - measure过程

作者: BlueSocks | 来源:发表于2023-07-28 16:12 被阅读0次

    1. View的工作流程概述

    View的绘制流程主要是指View的 measure layout draw 过程:

    • measure :确定View的测量宽/高

    • layout :确定View的最终宽高和四个顶点的位置

    • draw :将View绘制到屏幕上

    View的绘制流程是从ViewRootImplperformTraversals方法开始的,performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法:

    • performMeasure performMeasure会调用DecorView的measure方法,将measure流程传递到根View(DecorView)。因为DecorView继承自FrameLayout,FrameLayout继承自ViewGroup,ViewGroup继承自View,那么到DecorView之后的measure过程主要就是ViewGroup和View的measure过程。

    • performLayout 同performMeasure一样,调用DecorView的layout方法,将layout流程传递到根View(DecorView)。

    • performDraw: 同performMeasure一样,调用DecorView的draw方法,将draw流程传递到根View(DecorView)。

    2. measure过程概述

    measure过程主要是为了确定View的测量宽/高,理解View测量过程的重点在于理解MeasureSpec和onMeasure方法。

    • measure 方法
      measure方法是一个final方法(不能重写measure方法),measure方法会调用自己的onMeasure方法

    • View的 onMeasure 方法:
      根据自己的MeasureSpec确定测量宽/高。

    • ViewGroup onMeasure 方法:
      需要去遍历子元素,并调用子元素的measure方法,将measure过程传递到子元素。
      等子元素完成测量后,ViewGroup会根据自己的MeasureSpec和子元素的测量宽/高,来确定自身的测量宽/高。

    • MeasureSpec
      View测量宽/高的确定是和MeasureSpec有关的

    measure的传递过程:

    1. performMeasure调用DecorViewmeasure方法;
    2. DecorViewmeasure方法调用onMeasure方法,在onMeasure方法中遍历子元素,并调用子元素的measure方法
    3. 子元素measure方法被调用后,重复过程2,如此反复,完成整个View树的遍历。

    3. MeasureSpec

    View的Measure过程主要是为了确定View的测量宽/高,而View的测量宽/高的确定是和MeasureSpec有关的,所以在了解View的测量过程前,需要先了解MeasureSpec

    MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指测量模式,而SpecSize是指在某种模式下的规格大小。MeasureSpec通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。它的定义如下:

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        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;
    
        /**
         * 将SpecMode和SpecSize打包成一个int值,避免过多的对象内存分配
         * */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
        /**
         * 将打包后的int值解包,获取SpecMode
         * */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
    
        /**
         * 将打包后的int值解包,获取SpecSize
         * */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }
    
    

    SpecMode有三类,如下:

    • UNSPECIFIED
      父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态

    • EXACTLY
      父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

    • AT_MOST
      父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content

    4. MeasureSpec和LayoutParams的对应关系

    View的MeasureSpec由父容器和View自身的LayoutParams共同决定:

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

    4.1. DecorView的MeasureSpec

    先来看下DecorView的MeasureSpec的创建过程,在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

    /**
     * @param desiredWindowWidth    屏幕宽度
     * @param desiredWindowHeight   屏幕高度
     * @param lp                    WindowManager.LayoutParams
     */
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    

    接着看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.
                //窗口无法调整大小。强制根视图为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;
    }
    
    

    根据上述代码,DecorView的MeasureSpec的产生过程就很明确了,其由它的LayoutParams屏幕尺寸共同确定,如下:

    • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小

    • LayoutParams.WRAP_CONTENT:最大模式,大小不确定,但不能超过窗口的大小

    • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小

    4.2. 普通View的MeasureSpec

    对于普通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);
    }
    
    

    measureChildWithMargins方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,子元素MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的marginpadding有关,具体情况在ViewGroup的getChildMeasureSpec方法中,如下:

    /**
     * 根据父容器的MeasureSpec和View自身的LayoutParams来确定子元素的MeasureSpec
     * @param spec 父容器的MeasureSpec
     * @param padding 父容器已占用的空间大小
     * @param childDimension View本身的LayoutParams,如MATCH_PARENT、WRAP_CONTENT、100dp
     * @return 返回View的MeasureSpec
     * */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        int size = Math.max(0, specSize - padding);
    
        //View最终的SpecSize
        int resultSize = 0;
        //View最终的SpecMode
        int resultMode = 0;
    
        //判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                //判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 them have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == ViewGroup.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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    

    getChildMeasureSpec方法清楚的展示了普通View的MeasureSpec的创建规则。最后附上getChildMeasureSpec对应的一个表格梳理,其中parentSize是指父容器中目前可用的大小:

    父容器的SpecMode-EXACTLY 父容器的SpecMode-AT_MOST 父容器的SpecMode-UNSPECIFIED
    自身LayoutParams-固定值 EXACTLY
    childSize EXACTLY
    childSize EXACTLY
    childSize
    自身LayoutParams-match_parent EXACTLY
    parentSize AT_MOST
    parentSize UNSPECIFIED
    0
    自身LayoutParams-wrap_cotent AT_MOST
    parentSize AT_MOST
    parentSize UNSPECIFIED
    0

    5. LinearLayout的measure过程

    ViewGroup没有实现onMeasure方法,它提供了一个叫measureChildren的方法,measureChildren的实现比较简单,就是遍历所有的子元素,取出子元素的LayoutParams,然后再通过getChilMeasureSpec来创建子元素的MeasureSpec,最后将子元素的MeasureSpec直接传递给子元素的measure方法来进行测量。getChildMeasureSpec方法也在分析MeasureSpec的时候介绍过了。

    下面我们主要分析下LinearLayout的onMeasure方法,如下所示:

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

    LinearLayout竖直布局和水平布局的测量过程是类似的,分析其中一个就可以了,如下:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        //记录LinearLayout的总高度
        mTotalLength = 0;
        ...
    
        //遍历所有子元素
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            //获取子元素的LayoutParams
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
    
            ...
    
            //measureChildBeforeLayout方法调用了measureChildWithMargins方法,
            //measureChildWithMargins方法在介绍MeasureSpec的时候已经介绍过了,
            //就是根据LinearLayout的margin、MeasureSpec和子元素本身的LayoutParams
            //来确定子元素的MeasureSpec,并调用子元素的measure方法
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
    
            //measureChildBeforeLayout已经调用了子元素的measure方法,此时可以获取到子元素的测量高
            final int childHeight = child.getMeasuredHeight();
            final int totalLength = mTotalLength;
            //最后使用mTotalLength累加子元素的高度
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                    lp.bottomMargin + getNextLocationOffset(child));
            ...
        }
        ...
    
        //加上LinearLayout自身的padding,
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
        //计算LinearLayout的最终大小,
        // resolveSizeAndState方法就是根据LinearLayout的MeasureSpec和记录的mTotalLength
        // 得到LinearLayout最终的测量高
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ...
        //设置LinearLayout的测量宽/高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
    
    }
    
    

    在measureVertical方法中,LinearLayout遍历所有子元素,通过measureChildBeforeLayout确定子元素的MeasureSpec并调用子元素的measure方法,完成子元素的测量。在遍历过程中,还会根据子元素的高度来测量自己的大小,即mTotalLength,最后LinearLayout根据自身的MeasureSpec和mTotalLength确定自己的测量高,并使用setMeasuredDimension方法设置自己的测量高。

    6. View的Measure过程

    View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会调用View的onMeasure方法,onMeasure方法如下:

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

    setMeasuredDimension方法会设置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;
        //如果是AT_MOST或者EXACTLY,返回specSize。
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    
    

    getDefaultSize方法中主要根据View的specMode来确定View的测量宽/高,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况就可以了。在specMode是AT_MOST或者EXACTLY的时候,getDefaultSize返回的大小就是specSize,即View的测量宽/高就是specSize。在EXACTLY模式下,specSize是一个固定大小,而在AT_MOST模式下,specSize是多少?回顾上面根据MeasureSpec的创建规则:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //父容器的specMode
        int specMode = MeasureSpec.getMode(spec);
        //父容器的specSize
        int specSize = MeasureSpec.getSize(spec);
        //父容器可用大小
        int size = Math.max(0, specSize - padding);
    
        //判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                //判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
                ...
                else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            case MeasureSpec.AT_MOST:
                ...
                else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    

    可以发现,如果View在布局中使用wrap_cotent,,它的大小就是父容器可用大小,这种效果和在布局中使用match_parent一样,所以我们在自定义View的时候,要处理View宽/高是wrap_content的情况,重写View的onMeasure方法,如下:

    @Override
    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.getMode(heightMeasureSpec);
    
        //对于wrap_content情况,设置一个默认的宽/高
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mDefaultWidth, mDefaultHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mDefaultWidth, heightMeasureSpec);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthMeasureSpec, mDefaultHeight);
        }
        //其它情况还是沿用系统的测量值
    }
    
    

    相关文章

      网友评论

          本文标题:Android View的工作流程 - measure过程

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