Android View 工作原理

作者: ScottStone | 来源:发表于2018-11-29 14:19 被阅读30次

    介绍View的工作原理之前,先来介绍一些基础的概念,以便后面详细的介绍View的三大流程:measure、layout和draw。

    1.ViewRoot和DecorView

    ViewRoot具体对应的类是ViewRootImpl,该类是链接WindowManager与DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。ActivityThread中,Activity被创建之后,会把DecorView添加到Window中,同时创建ViewRootImpl对象,并给ViewRootImpl跟DecorView建立关联。

    View的绘制是从ViewRoot的performTraversals方法开始的,经过measure、layout和draw三个过程View才最终绘制出来。measure用来测量View的宽高,layout用来确定View在父容器中的位置,draw很明显是把View绘制在屏幕上。

    performTraversals流程

    如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw这个三个流程。其中:

    1.perfromMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父元素的measure过程,如此反复就完成整个View树的遍历。

    2.performLayout的传递流程和performMeasure是一样的。

    3.performDraw的传递过程是在draw方法中通过dispathDraw来实现的,本质上并没有区别。

    measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。

    layout最终决定了View的四个顶点的坐标和实际View的宽/高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点坐标位置,并可以通过getWidth和getHeight来得到View的最终宽高

    draw过程决定了View的显示,只有draw方法完成以后View的内容才会最终显示在屏幕上。

    简单介绍了三大过程之后,我们来看下DecorView的结构:

    image

    DecorView作为顶级View一般会包含一个垂直的LinearLayout,通常在这上面会有上下两部分(也需要根据具体情况来定),上面是标题栏,下面是内容栏。我们在Activity中setContentView就是添加在内容栏中,内容栏的id是content。大家也可以猜到,View层的时间都是先经过DecorView然后才会传给其他的View。

    2.MeasureSpec

    为了更好的了解View的测量过程,我们下面介绍下MeasureSpec。MeasureSpec在很大程度上决定了一个View的尺寸规格,当然除了MeasureSpec还会受到父容器的影响。

    MeasureSpec的中文意思是测量规格的意思,MeasureSpec代表一个32的int值,高2位代表SpecMode测量模式,低30位代表SpecSize测量规格大小。

    经常使用的三个函数:

    1.public static int makeMeasureSpec(int size,int mode)

    构造一个MeasureSpec

    2.public static int getMode(int measureSpec)

    获取MeasureSpec的测量模式

    3.public static int getSize(int measureSpec)

    获取MeasureSpec的测量大小

    SpecMode分为三类,每一类都没有他们对应的约束。

    1.UNSPECIFIED

    父容器不对View有任何限制,要多大就给多大,这种情况,一般我们自定义View用不到。

    2.Exactly

    这个表示准确值,这个对应着LayoutParams中的matchparent和准确值,这个时候View的最终大小就是SpecSize所指定的数。

    3.AT_MOST

    父容器指定了一个可用大小即SpecSize,View的大小不能大于该值,具体是什么要看View中自己的处理,所有自定义View时,我们需要自己在measure里面处理设置布局的大小,它对应layoutparams中的wrap_content

    这里还要提一下MeasureSpec跟LayoutParams的对应关系。

    使用MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置layoutparams,在View测量的时候,系统会将LayoutParams在父容器的约束下自动转化成对应的MeasureSpec,然后再根据MeasureSpec来确定View最终的宽高。

    MeasureSpec不是由LayoutParams唯一决定的,子View的宽高由自身的layoutparams和父容器的MeasureSpec。

    对于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams决定;

    对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;

    遵循的规则:

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

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

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

    **4.当view采用固定的宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小

    5.当View的宽高时matchparent时,如果父容器是精确模式,那么View也是精确模式,并且View也是精确模式并且大小是父容器的剩余空间。如果父容器是最大模式,那么View也是最大模式但是大小不能超过剩余的空间。

    6.当View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超过父容器的剩余空间。

    image

    3.View的工作流程

    1.Measure

    measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。下面我们再分别介绍下这两种情况下的过程。

    A.View的Measure过程,上源码(源码有方法的注释,对理解方法有很大的帮助,有兴趣可以看下):

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

    上面代码看起来很简洁,但是我们继续看里面的getDefaultSize方法。

     /**
         * Utility to return a default size. Uses the supplied size if the
         * MeasureSpec imposed no constraints. Will get larger if allowed
         * by the MeasureSpec.
         *
         * @param size Default size for this view
         * @param measureSpec Constraints imposed by the parent
         * @return The size this view should be.
         */
        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;
        }
    

    这里我们看到了上面setMeasuredDimension设置宽高的测量值的具体方法,getDefaultSize方法逻辑也很简单,我们重点看下AT_MOST跟EXACTLY这两种情况。简单地理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。

    UNSPECIFIED这种情况取值是通过getSuggestedMinimumHeight跟getSuggestedMinimumWidth,我们看下源码:

      /**
         * Returns the suggested minimum height that the view should use. This
         * returns the maximum of the view's minimum height
         * and the background's minimum height
         * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
         * <p>
         * When being used in {@link #onMeasure(int, int)}, the caller should still
         * ensure the returned height is within the requirements of the parent.
         *
         * @return The suggested minimum height of the view.
         */
        protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    
        }
    
        /**
         * Returns the suggested minimum width that the view should use. This
         * returns the maximum of the view's minimum width
         * and the background's minimum width
         *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
         * <p>
         * When being used in {@link #onMeasure(int, int)}, the caller should still
         * ensure the returned width is within the requirements of the parent.
         *
         * @return The suggested minimum width of the view.
         */
        protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
    

    以getSuggestedMinimumHeight为例如果View没有设置背景,那么View的高度为mMinHeight,而mMinWidth对应于android:minHeight这个属性所指定的值,因此View的宽度即为android:minHeight属性所指定的值。这个属性如果不指定,那么mMinHeight则默认为0;如果View指定了背景,则View的宽度为max(mMinHeight,mBackground.getMinimumHeight())。mMinHeight的含义我们已经知道了,那么mBackground.getMinimumHeight()是什么呢?我们看一下Drawable的getMinimumHeight方法

     /**
         * Returns the minimum height suggested by this Drawable. If a View uses this
         * Drawable as a background, it is suggested that the View use at least this
         * value for its height. (There will be some scenarios where this will not be
         * possible.) This value should INCLUDE any padding.
         *
         * @return The minimum height suggested by this Drawable. If this Drawable
         *         doesn't have a suggested minimum height, 0 is returned.
         */
        public int getMinimumHeight() {
            final int intrinsicHeight = getIntrinsicHeight();
            return intrinsicHeight > 0 ? intrinsicHeight : 0;
        }
    

    可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。那么Drawable在什么情况下会有原始宽度?这个是关于Drawable的问题,我们这里按下不表。

    getSuggestedMinimumWidth的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高。

    这里再说明一个问题,从getDefaultSize方法可以看出:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。

    Why??下面我们分析一下。

    如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize;根据上面普通View的MeasureSpec创建规则,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。简单点说就是你不处理,wrap_content跟match_parent就是一样的效果。

    那要怎么处理呢?我们先看下TextView是怎么做的。

    ......
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int width;
            int height;
    ......
    

    这里的widthSize、heightSize是默认值也就是测量大小,width、height是最终值,我们继续往下看。

    if (widthMode == MeasureSpec.EXACTLY) {
                // Parent has told us how big to be. So be it.
                width = widthSize;
            } else {
                if (mLayout != null && mEllipsize == null) {
                    des = desired(mLayout);
                }
    
                if (des < 0) {
                    boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                    if (boring != null) {
                        mBoring = boring;
                    }
                } else {
                    fromexisting = true;
                }
    
                if (boring == null || boring == UNKNOWN_BORING) {
                    if (des < 0) {
                        des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0,
                                mTransformed.length(), mTextPaint, mTextDir));
                    }
                    width = des;
                } else {
                    width = boring.width;
                }
    
                final Drawables dr = mDrawables;
                if (dr != null) {
                    width = Math.max(width, dr.mDrawableWidthTop);
                    width = Math.max(width, dr.mDrawableWidthBottom);
                }
    
                if (mHint != null) {
                    int hintDes = -1;
                    int hintWidth;
    
                    if (mHintLayout != null && mEllipsize == null) {
                        hintDes = desired(mHintLayout);
                    }
    
                    if (hintDes < 0) {
                        hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                        if (hintBoring != null) {
                            mHintBoring = hintBoring;
                        }
                    }
    
                    if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                        if (hintDes < 0) {
                            hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, 0, mHint.length(),
                                    mTextPaint, mTextDir));
                        }
                        hintWidth = hintDes;
                    } else {
                        hintWidth = hintBoring.width;
                    }
    
                    if (hintWidth > width) {
                        width = hintWidth;
                    }
                }
    
                width += getCompoundPaddingLeft() + getCompoundPaddingRight();
    
                if (mMaxWidthMode == EMS) {
                    width = Math.min(width, mMaxWidth * getLineHeight());
                } else {
                    width = Math.min(width, mMaxWidth);
                }
    
                if (mMinWidthMode == EMS) {
                    width = Math.max(width, mMinWidth * getLineHeight());
                } else {
                    width = Math.max(width, mMinWidth);
                }
    
                // Check against our minimum width
                width = Math.max(width, getSuggestedMinimumWidth());
    
                if (widthMode == MeasureSpec.AT_MOST) {
                    width = Math.min(widthSize, width);
                }
            }
    

    这个if/else中间省略了一部分代码,但是大家可以看到,wrap_content这种情况下,给了一个默认值,计算过程,大家有兴趣可以具体看下,其实也就是说,解决这个问题只需要给一个默认值,但是这个默认值大小并没有固定的依据,我觉得根据自己实际的View的使用情况来定即可。

    B.ViewGroup的measure过程

    ViewGroup除了完成自己的Measure之外还会遍历子View的Measure方法,每个子View再递归的完成这个过程。ViewGroup是一个抽象类,因此并没有重写onMeasure方法,但是提供了measureChildren方法,源码如下:

     /**
         * Ask all of the children of this view to measure themselves, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * We skip children that are in the GONE state The heavy lifting is done in
         * getChildMeasureSpec.
         *
         * @param widthMeasureSpec The width requirements for this view
         * @param heightMeasureSpec The height requirements for this view
         */
        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);
                }
            }
        }
        /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * The heavy lifting is done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param parentHeightMeasureSpec The height requirements for this 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会对每个子View进行measure,具体可以看下measureChild方法,如上图。很显然,measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec的工作过程已经在上面进行了详细分析。

    ViewGroup的Measure方法可以看下LinearLayout

      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (mOrientation == VERTICAL) {
                measureVertical(widthMeasureSpec, heightMeasureSpec);
            } else {
                measureHorizontal(widthMeasureSpec, heightMeasureSpec);
            }
        }
    
     // See how tall everyone is. Also remember max width.
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }
    
                if (child.getVisibility() == View.GONE) {
                   i += getChildrenSkipCount(child, i);
                   continue;
                }
    
                nonSkippedChildCount++;
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
    
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
                totalWeight += lp.weight;
    
                final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
                if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                    // Optimization: don't bother measuring children who are only
                    // laid out using excess space. These views will get measured
                    // later if we have space to distribute.
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                    skippedMeasure = true;
                } else {
                    if (useExcessSpace) {
                        // The heightMode is either UNSPECIFIED or AT_MOST, and
                        // this child is only laid out using excess space. Measure
                        // using WRAP_CONTENT so that we can find out the view's
                        // optimal height. We'll restore the original height of 0
                        // after measurement.
                        lp.height = LayoutParams.WRAP_CONTENT;
                    }
    
                    // 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();
                    if (useExcessSpace) {
                        // Restore the original height and record how much space
                        // we've allocated to excess-only children so that we can
                        // match the behavior of EXACTLY measurement.
                        lp.height = 0;
                        consumedExcessSpace += childHeight;
                    }
    
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(child));
    
                    if (useLargestChild) {
                        largestChildHeight = Math.max(childHeight, largestChildHeight);
                    }
                }
    ......
    

    从上面这段代码可以看出,系统会遍历子元素并对每个子元素执行measureChild-BeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,源码如下所示。

    ......
     // 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;
            // Either expand children with weight to take up available space or
            // shrink them if they extend beyond our current bounds. If we skipped
            // measurement on any children, we need to measure them now.
            int remainingExcess = heightSize - mTotalLength
                    + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
    ......
    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);
            }
    ......
    

    从上面可以看出,当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程则和View有所不同。具体来说是指,如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向的padding,这个过程可以进一步参看如下源码:

        /**
         * Utility to reconcile a desired size and state, with constraints imposed
         * by a MeasureSpec. Will take the desired size, unless a different size
         * is imposed by the constraints. The returned value is a compound integer,
         * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
         * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
         * resulting size is smaller than the size the view wants to be.
         *
         * @param size How big the view wants to be.
         * @param measureSpec Constraints imposed by the parent.
         * @param childMeasuredState Size information bit mask for the view's
         *                           children.
         * @return Size information bit mask as defined by
         *         {@link #MEASURED_SIZE_MASK} and
         *         {@link #MEASURED_STATE_TOO_SMALL}.
         */
        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) {
                        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);
        }
    

    View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasured-Width/Height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

    这里引申一个问题,Activity启动之后,某个任务需要View的宽高,如何获取View的宽高呢?

    这个场景还是很常见的,我们这里先分析下上面这个问题,我们知道,onCreate、onStart、onResume中均无法正确得到某个View的宽/高信息,因为View的Measure过程跟Activity的生命周期不是同步执行的,有没有办法解决这个问题呢?肯定是有的,下面我们就介绍几种方法。

    (1)Activity/View#onWindowFocusChanged。

    onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。

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

    3)ViewTreeObserver。使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发现改变时,onGlobalLayout方法将被回调,因此这是获取View的宽/高一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。

    (4)view.measure。通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分。

    上面四个方法具体可以参考 任玉刚.Android开发艺术探索.电子工业出版社 4.3.1

    2.Layout

    Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法,如下所示。

    /**
         * Assign a size and position to a view and all of its
         * descendants
         *
         * <p>This is the second phase of the layout mechanism.
         * (The first is measuring). In this phase, each parent calls
         * layout on all of its children to position them.
         * This is typically done using the child measurements
         * that were stored in the measure pass().</p>
         *
         * <p>Derived classes should not override this method.
         * Derived classes with children should override
         * onLayout. In that method, they should
         * call layout on each of their children.</p>
         *
         * @param l Left position, relative to parent
         * @param t Top position, relative to parent
         * @param r Right position, relative to parent
         * @param b Bottom position, relative to parent
         */
        @SuppressWarnings({"unchecked"})
        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方法的大致流程如下:

    (1)通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了。

    (2)接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

    接下来,我们可以看一下LinearLayout的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);
            }
        }
    

    我们具体看下layoutVertical:

    /**
         * Position the children during a layout pass if the orientation of this
         * LinearLayout is set to {@link #VERTICAL}.
         * @see #getOrientation()
         * @see #setOrientation(int)
         * @see #onLayout(boolean, int, int, int, int)
         * @param left
         * @param top
         * @param right
         * @param bottom
         */
        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方法中完成自己的定位以后,就通过onLayout方法去调用子元素的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);
        }
    
    

    引申一个问题View的测量宽/高和最终/宽高有什么区别?

    这个问题实际上是:View的getMeasuredWidth和getWidth这两个方法有什么区别?回答这个问题我们还是上源码,看一下getwidth和getHeight这两个方法的具体实现:

    /**
         * Return the width of the your view.
         *
         * @return The width of your view, in pixels.
         */
        @ViewDebug.ExportedProperty(category = "layout")
        public final int getWidth() {
            return mRight - mLeft;
        }
        /**
         * Return the height of your view.
         *
         * @return The height of your view, in pixels.
         */
        @ViewDebug.ExportedProperty(category = "layout")
        public final int getHeight() {
            return mBottom - mTop;
        }
    

    其实看出,在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为View的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,也就是在layout过程中改变了宽高会导致最终的宽高发生变化。

    3.Draw

    Draw过程就比较简单了,很明显它的作用是将View绘制到屏幕上面。先上源码:

     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的绘制过程概括下遵循如下几步:

    (1)绘制背景background.draw(canvas)。

    (2)绘制自己(onDraw)。

    (3)绘制children(dispatchDraw)。

    (4)绘制装饰(onDrawScrollBars)。

    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);
        }
    
    ...
    //ViewGroup
     private void initViewGroup() {
            // ViewGroup doesn't draw by default
            if (!debugDraw()) {
                setFlags(WILL_NOT_DRAW, DRAW_MASK);
            }
    ...
    

    从源码可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。从上面View跟ViewGroup的代码也可以看到,View默认是没有启用这个优化的,而ViewGroup是默认启用了。这里就引申出一个问题:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。同样,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。

    扩展:View的重绘与更新

    我们知道View的重绘与更新,可以调用invalidate方法或者是requestLayout方法,两者有什么区别呢?我们看下源码注释:

     /**
         * Invalidate the whole view. If the view is visible,
         * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
         * the future.
         * <p>
         * This must be called from a UI thread. To call from a non-UI thread, call
         * {@link #postInvalidate()}.
         */
        public void invalidate() {
            invalidate(true);
        }
    
        /**
         * This is where the invalidate() work actually happens. A full invalidate()
         * causes the drawing cache to be invalidated, but this function can be
         * called with invalidateCache set to false to skip that invalidation step
         * for cases that do not need it (for example, a component that remains at
         * the same dimensions with the same content).
         *
         * @param invalidateCache Whether the drawing cache for this view should be
         *            invalidated as well. This is usually true for a full
         *            invalidate, but may be set to false if the View's contents or
         *            dimensions have not changed.
         * @hide
         */
        public void invalidate(boolean invalidateCache) {
            invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
        }
    ......
     /**
         * Call this when something has changed which has invalidated the
         * layout of this view. This will schedule a layout pass of the view
         * tree. This should not be called while the view hierarchy is currently in a layout
         * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
         * end of the current layout pass (and then layout will run again) or after the current
         * frame is drawn and the next layout occurs.
         *
         * <p>Subclasses which override this method should call the superclass method to
         * handle possible request-during-layout errors correctly.</p>
         */
        @CallSuper
        public void requestLayout() {
    ......
    

    从上面源码的注释中可以看到,invalidate方法是重新绘制更新View,重新调用draw方法。requestLayout方法则是只调用Measure跟Layout方法。invalidate的注释也写了,其只能是UI线程调用,非UI线程需要用postInvalidate方法。一般的,View如果appearance发生改变,重新调用invalidate方法就可以,如果View是位置、大小发生变化则需要调用requestLayout。两者都有变化,一般先调用requestLayout再调用invalidate。

    到这里View的工作原理就介绍完了,如果大家想自定义自己的View相信会从上面的介绍中有心得体会。

    参考:任玉刚.Android开发艺术探索.电子工业出版社

    相关文章

      网友评论

        本文标题:Android View 工作原理

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