View 工作原理

作者: 5260fbd1e4e1 | 来源:发表于2018-07-28 12:12 被阅读24次

    1、 ViewRoot 和 DecorView 介绍

    ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowMnager 类 和 DecorView 的纽带,View 的三大流程是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联,这个过程可参考如下源码:

        root = new ViewRootImpl(view.getContext(),display);
        root.setView(view,wparams,panelParentView);
    

    view 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,经过 measure、layout 和 draw 三个过程才能将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
    而 performTraversals 的流程如下:


    performTraversals 工作流程.png

    由上图可知,performTraversals 会依次调用 performMeasure、performLayout 和 performDraw方法。这三个方法会完成顶级 View 的 measure、layout 和 draw 这三个流程。
    其中,performMeasure 中会调用 measure 方法,在measure 方法中又会调用 onMeasure,然后在 onMeasure 才会对所用子元素进行 measure 过程,这时候,就从父容器传递到了子元素,也就是完成了一次 measure 过程,接着子元素会重复父容器的 measure 过程,如此反复直到完成整个 View 树的遍历。performLayout 和 performDraw 类似,唯一不同的是,performDraw 的传递 过程是在 draw 方法中,通过 dispatchDraw 来实现的。

    measure 过程决定了 View 的宽高,Measure 完成之后,可以通过 getMeasureWdith 和 getMeasureHeight 方法获取到 View 测量后的宽高,一般情况下,这就是 View 最终的宽高。Layout 过程决定了 View 四个顶点的坐标和最终 View 的宽高,完成之后,可通过 getTop、getBottom、getLeft、getRight 来得到 View 四个顶点的位置,并且可以通过 getWidth 和 getHeight 来得到 View 最终的宽高。最后,Draw 方法决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。

    DecorView 的结构:


    decorView.png

    如图所示,DecorView 做为顶级 View,一般情况下它的内部都会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两个部分(具体情况和 Android 版本及主题有关)。我们设置的布局就是下面的内容栏,其 id 为 content。我们可以通过:

      ViewGroup content = findViewById(R.android.id.content);
    

    得到这个 content,然后我们可以通过 content.getChildAt(0) 得到我们设置的 View。我们还需要知道,DecorView 是一个 Fragment,View 的事件多要经过DecorView 才能传递给我们的 View。

    2、理解 MeasureSpec

    为了更好的理解 View 的测量过程,我们还要理解 MeasureSpec。MeasureSpec 决定了 View 的规格尺寸,但是 MeasureSpec 还受到父容器的影响。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个 measureSpec 来测量出 View 的宽高。

    2.1 MeasureSpec

    MeasureSpec 代表了一个 32 位的 int 值,高 2 位 代表 SpecMode,低 30 位代表 SpecSize。

    • SpecMode 是测量模式
    • SpecSize 是测量尺寸
      MeasureSpec:
    
    /**
     * MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求
     * MeasureSpec由size和mode组成。
     * 三种Mode:
     * 1.UNSPECIFIED
     * 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
     * (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
     * 2.EXACTLY
     * 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
     * (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
     * 3.AT_MOST
     * 子最大可以达到的指定大小
     * (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
     * 
     * MeasureSpecs使用了二进制去减少对象的分配。
     */
    public class MeasureSpec {
            // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
            private static final int MODE_SHIFT = 30;
            
            // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
            // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
     
            // 0向左进位30,就是00 00000000000(00后跟30个0)
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
            // 1向左进位30,就是01 00000000000(01后跟30个0)
            public static final int EXACTLY     = 1 << MODE_SHIFT;
            // 2向左进位30,就是10 00000000000(10后跟30个0)
            public static final int AT_MOST     = 2 << MODE_SHIFT;
     
            /**
             * 根据提供的size和mode得到一个详细的测量结果
             */
            // measureSpec = size + mode;   (注意:二进制的加法,不是十进制的加法!)
            // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
            // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
            public static int makeMeasureSpec(int size, int mode) {
                return size + mode;
            }
     
            /**
             * 通过详细测量结果获得mode
             */
            // mode = measureSpec & MODE_MASK;
            // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
            // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
            public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
            }
     
            /**
             * 通过详细测量结果获得size
             */
            // size = measureSpec & ~MODE_MASK;
            // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
            public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
     
            /**
             * 重写的toString方法,打印mode和size的信息,这里省略
             */
            public static String toString(int measureSpec) {
                return null;
            }
    }
    

    代码中有详细注释,主要说一下 SpecMode:

    • UNSPECFIED
      父容器不对 View 有任何限制,要多大给多大,一般用于系统内部。
    • EXACTLY
      父容器已经检测出 View 所需要的大小,这时候 View 的最终大小就是 SpecSize 所指定的值。这个模式对应 LayoutParams 中的 match_parent和具体数值两种模式。
    • AT_MOST
      父容器制定了一个可用大小即 SpecSize,这个时候, View 的最终大小不能大于这个值,具体是多少要看 View 的具体实现,对于LayoutParams 中的 wrap_content。
    2.2 MeasureSpec 和 LayoutParams 的对应关系

    系统内部是通过 MeasureSpece 来进行 View 的测量,但是正常情况下我们使用 View 指定 MeasureSpec,尽管如此,我们给 View 设置 LayoutParams。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后在根据这个 MeasureSpec 来确定 View 测量后的 宽高。需要注意的是 MeasureSpec 不是唯由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec ,进而进一步决定 View 的宽高。另外,对于顶级 View (DecorView) 和 普通 View 来说,MeasureSpec 的转换过程略有不同。对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定。
    对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 来共同决定,MeasureSepc 一旦确定后,onMeasure 就可以确定 View 的测量的宽高。

    对于 DecorView 来说,在 ViewRootImp 中的 measureHierachy 方法中有如下代码,它展示了 DecorView 的 MeasureSpec 的创建过程,其中 desiredWindowWidth 和 desiredWindowHeight 和屏幕尺寸:

        if (!goodMeasure) {
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                    windowSizeMayChange = true;
                }
            }
    

    getRootMeasureSpec 方法如下:

        /**
         * Figures out the measure spec for the root view in a window based on it's
         * layout params.
         *
         * @param windowSize
         *            The available width or height of the window
         *
         * @param rootDimension
         *            The layout params for one dimension (width or height) of the
         *            window.
         *
         * @return The measure spec to use to measure the root view.
         */
        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;
        }
    

    通过上述代码,DecorView 的 MeasureSpec 的产生过程遵守如下规则,根据它的 LayoutParams 中的宽/高参数来划分:

    • LayoutParams.MATCH_PARENT
      精确模式,大小就是窗口的大小。
    • LayoutParams.WRAP_CONTENT:
      最大模式,大小不定,但是不能超过窗口大小。
    • 固定大小(如 100 dp):精确模式,大小为 LayoutParams 中指定的大小。

    对于我们布局中的 View, View 的 measure 过程由 ViewGroup 传递而来,ViewGroup 的 measureChildWidthMargins 方法如下:

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

    上述方法会对子元素进行 measure,在调用 子元素的 measure 方法前,会通过 getChildMeasureSpec 方法得到子元素的 MeasureSpec。具体来说,子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 View 的 margin 及 padding 有关,getChildMeasureSpec 方法如下:

        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);
            int resultSize = 0;
            int resultMode = 0;
            switch (specMode) {
            // Parent has imposed an exact size on us
            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) {
                    // Child wants to be our size... find out how big it should
                    // be
                    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;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    这个方法的主要作用就是根据父容器的 MeasureSpec 结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec,参数 padding 为父容器已经占用的空间,因此子元素可用大小为父容器的尺寸减去 padding,具体代码如下:

            int specSize = MeasureSpec.getSize(spec);
            int size = Math.max(0, specSize - padding);
    

    根据代码,普通 View 的创建规则如下:


    View 的 measureSpec 创建规则.png

    主要规则如下:

    • 当View采用固定宽/高时,不管父容器的Measure是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams中的大小。
    • 当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
    • 当View的宽/高是wrap_content时,不管父容器是最大模式还是精确模式,View的模式总是最大模式,并且其大小不会超过父容器的剩余空间。
    • UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说,不需要关注此模式。

    根据上表,我们只要指定 父容器的 MeasureSpec 和子元素的 LayoutParams ,就可以快速的确定子元素的 MeasureSpec,有了 MeasureSpec 就可以进一步确定子元素测量后的大小。

    3、View 的工作流程

    View 的工作流程主要是 measure、layout、draw 三大流程,也就是测量,布局和绘制。
    measure: 确定 View 的测量宽高。
    layout: 确定 View 的最终宽高和四个顶点的位置。
    draw: 将 View 绘制到屏幕上。

    3.1 View 的measure 过程

    View 的 measure 过程有 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着此类方法不能被子类重写,在 View 的 measure 方法中会去调用 View 的 onMeasure 方法,因此只需要看 onMeasure 的实现,View 的 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;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    

    由上可知,getDefaultSize 这个方法逻辑很简单,对我们来说,只需要 AT_MOST 和 EXACTLY 这两种情况。简单的理解,getDefaultSize 返回的大小就是 measureSpec 和 specSize,而这个 specSize 就是 View 测量后的大小,测量后的大小是在 layout 阶段决定的,这里要加以区分,但是几乎全部情况下 View 的测量大小和最终大小是相等的。
    至于 UNSPECCIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下, View 的大小为 getDefaultSize 的第一个参数 size,即宽高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,源码如下:

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

    这里只分析,getSuggestedMinimum 方法的实现,从其代码可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,如果这个属性不指定,那么 mMinWidth 则默认为 0,如果 View 指定了背景,则 View 的宽度为 max(mMinWidth,mBackgroudn.getMinimumWIdth())。
    Drawable 的 getMinimumWidth 方法

        public int getMinimumWidth() {
            final int intrinsicWidth = getIntrinsicWidth();
            return intrinsicWidth > 0 ? intrinsicWidth : 0;
        }
    

    从上面可以看出,getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。

    getSuggestedMinimumWidth 的逻辑总结如下:如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以是 0,如何 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值。getSuttestedMinimumWidth 个 getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽高。
    从 getDefaulSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论:直接继承 View 的自定义控件需要重写onMeasure 方法并设置 wrap_content 的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,这种情况下, View 的 specSize 是 parentSize,而 parentSize 是父容器目前可以使用的大小,也就是父容器当前剩余的空间的大小,很显然, View 的宽高就等于父容器当前空间的大小,这种效果和在布局中使用 match_parent 完全一致,为了解决这个问题,在onMeasure 中会给 View 一个默认的宽高值:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
            int defaultWidth = 100;
            int defaultHeight = 100;
            if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(defaultWidth, defaultHeight);
            } else if (widthSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(defaultWidth, heightSpecSize);
            } else if (heightSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSpecSize, defaultHeight);
            } else {
                widthSpecSize = Math.min(widthSpecSize, heightSpecSize);
                heightSpecSize = Math.min(widthSpecSize, heightSpecSize);
                setMeasuredDimension(widthSpecSize, heightSpecSize);
            }
        }
    
    
    

    上面代码中,给 View 指定了一个默认的内部宽高值(mWidth 和 mHeight),并在 wrap_content 时设置这个宽高,对用非 wrap_content 情形,沿用系统的测量值即可。

    ViewGroup 的 measure 过程

    对于 ViewGroup 来说,除了完成自己的 measure 过程之外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是, ViewGroup 是一个抽象类,因此没有重写 View 的 onMeasure 方法,但是它提供了一个 measureChildren 的方法,如下:

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

    由上面的代码可知, ViewGroup 会对每个子元素进行 measure,measureChild 方法如下:

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

    上面代码的主要思想就是,取出子元素的 LayoutParams,然后通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量。

    我们知道,ViewGroup 没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout 、Relativelayout 等,这是因为不同的 ViewGroup 子类有不同的布局特性,因此其细节各不相同。
    LinearLayout 的 onMeasrue 方法如下:

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

    直接看竖直布局的测量过程:

    // 代码省略 ...
                for (int i = 0; i < count; ++i) {
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }
                    final LayoutParams lp = (LayoutParams)child.getLayoutParams();
                    final float childWeight = lp.weight;
      
     measureChildBeforeLayout(child,i,widhtMeasureSpec,0,heightMeasureSpec,totalWeight == 0 ? mTotalLenght : 0);
    
    if( oldHeight != Integer.MIN_VALUE){\
      lp.height  = oldHeight;
    }
    
                        final int childHeightMeasureSpec = 
                   MeasureSpec.makeMeasureSpec(
                                Math.max(0, childHeight), MeasureSpec.EXACTLY);
                        final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                                lp.width);
                        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                        // Child may now not fit in vertical dimension.
                        childState = combineMeasuredStates(childState, child.getMeasuredState()
                                & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                    }
    
                    final int margin =  lp.leftMargin + lp.rightMargin;
                    final int measuredWidth = child.getMeasuredWidth() + margin;
                    maxWidth = Math.max(maxWidth, measuredWidth);
    
                    boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                            lp.width == LayoutParams.MATCH_PARENT;
    
                    alternativeMaxWidth = Math.max(alternativeMaxWidth,
                            matchWidthLocally ? margin : measuredWidth);
    
                    allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
    
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
                }
    

    其测量过程的代码较多,主要思想就是,系统会遍历子元素并对子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初步高度,没测量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方法的 margin 等。当子元素测量完成后,LinearLayout 会测量自己的大小,源码大致如下:

        // Add in our padding
                mTotalLength += mPaddingTop + mPaddingBottom;
        ...
       if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
                maxWidth = alternativeMaxWidth;
            }
    
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // Check against our minimum width
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
            if (matchWidth) {
                forceUniformWidth(count, heightMeasureSpec);
            }
    

    View 的 measure 过程是三大流程中最复杂的一个, measure 完成之后,通过 getMeasuredWidht/height 方法就可以得到 View 的测量宽高。

    现在考虑一个情况,当我们要在 onCreate 或者 onResume 中获取 View 的宽高时,这样是无法正确得到 View 的宽高信息的,因为 View 的 measure 过程和 Activity 的周期不是同步的,目前有四种方法解决这个问题:

    • Activity/View #onWindowFocusChanged
      onWindowFocusChanged 这个方法的含义是: View 已经初始化完毕了,宽高已经准备好了,这时候去获取宽高是没有问题的。需要注意的时,这个方法会被多次调用,具体来说就是,Activity 继续执行和暂停执行都会调用这个方法。
      其使用方法如下:
    
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus) {
                       int height = view.getMeasureHeight();  
                       int widht = view.getMeasureWidth();
            } else {
                Log.e(Tag, "onWindowFocusChanged:" + "false");
            }
        }
    
    
    • 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();
            }
        });
    }
    
    • ViewTreeObserver
      通过使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的机会。值得注意的是,伴随着View树的状态改变等,onGlobalLayout会被多次调用。代码如下:
    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = tv.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
            }
        });
    }
    
    • view.measure(int widthMeasureSpec,int heightMeasureSpce)
      通过手动对 View 进行 measure 来得到 View 的宽高,这种方法比较复杂,要分情况处理,根据 View 的 LayoutParams 来分:

    (1) match_parent:
    直接放弃,因为无法知道 parentSize,而 measure 过程需要知道这个值。

    (2 )具体的数值(dp/dx):
    比如宽高都是 100px,如下 measure:

    int widthMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpce(100,MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    

    (3) wrap_content
    如下 measure:

    int widthMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.AT_MOST);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpce((1 << 30) -1,MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    

    因为 View 的尺寸使用 30 为二进制表示,也就是说最大是 30 个1(即 2^30 - 1),也就是 (1 << 30) -1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理。

    3.2 View 的 layout 过程

    Layout 的作用 用来确定子元素的位置,当 ViewGroup 的位置确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。layout 方法主要是确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。
    layout 方法如下:

      public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
    
                if (shouldDrawRoundScrollbar()) {
                    if(mRoundScrollbarRenderer == null) {
                        mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                    }
                } else {
                    mRoundScrollbarRenderer = null;
                }
    
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
            if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
                mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                notifyEnterOrExitForAutoFillIfNeeded(true);
            }
        }
    

    layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom
    这四个值,View 的四个顶点确定之后,那么 View 在父容器中的位置也就确定了,接着调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法,接下来我们看一下 LinearyLayout 中 onLayout 是如何实现的:

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (mOrientation == VERTICAL) {
                layoutVertical(l, t, r, b);
            } else {
                layoutHorizontal(l, t, r, b);
            }
        }
    
    

    也就是 onLayout 的实现还是和布局有关,这里以垂直为例:

        void layoutVertical(int left, int top, int right, int bottom) {
            final int paddingLeft = mPaddingLeft;
    
            int childTop;
            int childLeft;
    
            // Where right end of child should go
            final int width = right - left;
            int childRight = width - mPaddingRight;
    
            // Space available for child
            int childSpace = width - paddingLeft - mPaddingRight;
    
            final int count = getVirtualChildCount();
    
            final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
            final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
            switch (majorGravity) {
               case Gravity.BOTTOM:
                   // mTotalLength contains the padding already
                   childTop = mPaddingTop + bottom - top - mTotalLength;
                   break;
    
                   // mTotalLength contains the padding already
               case Gravity.CENTER_VERTICAL:
                   childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
                   break;
    
               case Gravity.TOP:
               default:
                   childTop = mPaddingTop;
                   break;
            }
    
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {
                    final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
    
                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();
    
                    int gravity = lp.gravity;
                    if (gravity < 0) {
                        gravity = minorGravity;
                    }
                    final int layoutDirection = getLayoutDirection();
                    final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                    switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                    + lp.leftMargin - lp.rightMargin;
                            break;
    
                        case Gravity.RIGHT:
                            childLeft = childRight - childWidth - lp.rightMargin;
                            break;
    
                        case Gravity.LEFT:
                        default:
                            childLeft = paddingLeft + lp.leftMargin;
                            break;
                    }
    
                    if (hasDividerBeforeChildAt(i)) {
                        childTop += mDividerHeight;
                    }
    
                    childTop += lp.topMargin;
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                    i += getChildrenSkipCount(child, i);
                }
            }
        }
    

    这个方法的主要逻辑如下:首先遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置到靠下的位置,这刚好符合垂直 LinearLayout 的特性,而下面的 setChildFrame 只是调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样依次调用就确定了整个 View 树的 layout 过程。
    setChildFrame 如下:

        private void setChildFrame(View child, int left, int top, int width, int height) {
            child.layout(left, top, left + width, top + height);
        }
    

    setChildFrame 中的 width 和 height 实际上就是子元素的测量宽高,上面的代码如下:

                     final int childWidth = child.getMeasuredWidth();
                     final int childHeight = child.getMeasuredHeight();
                    ....
                    setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                            childWidth, childHeight);
    

    而在 layout 中,会通过 setFrame 去设置子元素的四个顶点的位置,在 setFrame中有如下赋值语句:

    
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
    

    这样一来 子元素的位置就确定了。
    现在说一下 View 的测量宽高和最终宽高有什么区别,也就是 View 的 getMeasureWidth 和 getWidth 有什么区别。先看一下 getWidth 和 getHeight 的方法的实现:

       public final int getWidth() {
            return mRight - mLeft;
        }
    
        public final int getHeight() {
            return mBottom - mTop;
        }
    

    也就是 getWidth 和 getHeight 返回的是测量宽高,虽然在 View 的默认实现中,测量宽高和最终宽高是相等的,但是需要知道的是,测量宽高形成与 measure 过程,而最终宽高形成与 layout 过程,也就是赋值时机不一样,我们在日常开发中,可以认为这两个是相等的。
    下面举例说明为什么是不相等的:

    public void layout(int l,int t,int r,int b){
    super.layout(l,t,r+100,b+100);
    }
    

    重写 layout 方法,导致最终宽高比测量的大 100px。

    3.3 View 的 draw 过程

    draw 过程比较简单,主要作用就是将 View 绘制到屏幕上面,View的 draw 过程如下:

    • 绘制背景 background.draw(canvas)。
    • 绘制自己 (onDraw)。
    • 绘制 children (dispatchDraw)。
    • 绘制装饰 (onDrawScrollBars)。
      draw 源码如下:
        public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */
    
            // Step 1, draw the background, if needed
            int saveCount;
    
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                drawAutofilledHighlight(canvas);
    
                // Overlay is part of the content and draws beneath Foreground
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // Step 6, draw decorations (foreground, scrollbars)
                onDrawForeground(canvas);
    
                // Step 7, draw the default focus highlight
                drawDefaultFocusHighlight(canvas);
    
                if (debugDraw()) {
                    debugDrawFocus(canvas);
                }
    
                // we're done...
                return;
            }
            ...
        
        }
    

    View 的绘制过程是通过 dispatchDraw 来实现的, dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 会一层一层的传递下去,View 有一个特殊的方法,setWillNotDraw,源码如下:

        /**
         * If this view doesn't do any drawing on its own, set this flag to
         * allow further optimizations. By default, this flag is not set on
         * View, but could be set on some View subclasses such as ViewGroup.
         *
         * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
         * you should clear this flag.
         *
         * @param willNotDraw whether or not this View draw on its own
         */
        public void setWillNotDraw(boolean willNotDraw) {
            setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
        }
    

    也就是,一个 View 如果不需要绘制任何内容,那么设置这个标记位为 true 后,系统会相应的进行优化,View 默认没有启用这个标记位,ViewGroup 默认启用这个标记位。这个标记位的意义是:当我们自定义控件继承 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记以便于系统优化。相反,我们的 ViewGroup 需要绘制时,要手动关闭这个标记位。

    具体参考 《Android 开发艺术探索》View 工作原理。

    相关文章

      网友评论

        本文标题:View 工作原理

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