Android View工作原理详解(二)—三大流程

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

measure过程

measure过程要分两种情况来看,如果是View,那么通过measure方法就完成了其测量的过程,如果是VIewGroup,除了完成自己的测量之外,还要遍历调用所有子View的measure方法,如果子View仍是ViewGroup的话就递归这个过程。下面从源码去分析View和ViewGroup的measure过程。

View的measure过程

虽然View的测量过程是从measure方法开始的,但是measure方法是一个final方法,我们没办法重写,所有View都一样,主要做些测量的条件判断和初始化工作,最终会调用onMeasure方法。我们在实际中自定义View的触及不到,所以我们直接从onMeasure方法开始看,以下是View的onMeasure源码:

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

从源码来看,onMeasure方法很简洁,但不要小瞧这几行代码,完成了默认的View的 测量。我们一个一个来看,方法参数widthMeasureSpec,heightMeasureSpec是父控件的MeasureSpec,关于MeasureSpec上篇文章已经详细聊过,接下来我们看方法内实现,从setMeasuredDimension方法开始

/**
     * 

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

* * @param measuredWidth The measured width of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. * @param measuredHeight The measured height of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. */ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); } private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }

boolean optical = isLayoutModeOptical(this);
这句意思是先判断下当前View是不是属于ViewGroup,如果为false就命中下面的条件语句进行值计算,最后调用setMeasuredDimensionRaw()将值进行保存。从源码中我们看出setMeasuredDimension方法作用是将计算的宽高做最后调整后保存。
接下来我们看setMeasuredDimension方法的参数,将getSuggestedMinimumWidth()/getSuggestedMinimumHeight()和父控件的MeasureSpec通过getDefaultSize得来,我们看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;
    }

可以看出,getDefaultSize的逻辑很简单,就是通过父控件的MeasureSpecMode的三种情况来计算具体的测量值。
当MeasureSpec.UNSPECIFIED时,结果为getSuggestedMinimumWidth;
当MeasureSpec.AT_MOST和MeasureSpec.EXACTLY时,结果为MeasureSpec.getSize(measureSpec)。和我们在Android View工作原理详解(一)中说的计算MeasureSpec的方式一致。

我们再来看看getSuggestedMinimumWidth和getSuggestedMinimumHeight两个方法

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()}).
     * 

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

两个方法逻辑相同,我们以getSuggestedMinimumWidth来分析,从代码中我们可以看出:
1.如果View没有设置背景,那么View的宽度即为mMinWidth,而mMinWidth 对应android:mMinWidth 这个控件属性,如果不指定,默认值为0。
2.如果View设置了背景,view的宽度为max(mMinWidth, mBackground.getMinimumWidth(),mMinWidth的值我们已经知道了,那么mBackground.getMinimumWidth()的值又是什么呢,这里的mBackground是一个Drawable对象,我们看看Drawable的getMinimumWidth方法

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

计算规则是如果图片有原始宽度,就返回原始宽度,否则返回0,所以mBackground.getMinimumWidth()取的是背景图的原始大小宽度。

ViewGroup的measure过程

和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的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);
            }
        }
    }

从上述代码来看,ViewGroup在measure的过程中会调用measureChid方法去测量每个子VIew,我们来看看measureChid方法:

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

从代码来看,measureChild这个方法也很简单,首先取出当前的child View的LayoutParams,然后结合parent的MeasureSpec计算出自己的MeasureSpec(我们在Android View工作原理详解(一)说过自己的MeasureSpec是通过父控件的MeasureSpec和自身的layoutParams计算出来的),最后调用child的measure方法进行子View的测量。
如果只看ViewGroup的measure过程还不能完全理解容器控件的measure过程,毕竟ViewGroup是一个抽象类,很多实现都在其子类中实现,我们以LinearLayout为例,来看看LinearLayout的measure过程。

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

上面代码是LinearLayout的onMeasure方法源码,通过布局方向来选择measure过程。measureVerticalf方法太长,我们截取部分看下

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

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

从代码可以看出,LinearLayou会遍历元素对每个子元素执行MeasureChildBeforeLayout方法,这个方法内部会地调用子元素的measure方法,这样各个子元素就开始依次进行measure过程,通过全局变量mTotalLength 保存每一行的的高度之和,当子View测量完成之后会测量自己的大小:

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
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);
    }

getMeasuredWidth/Height

View的measure过程是三大流程中最复杂的一个,measure完成之后就可以通过getMeasuredWidth/Height方法就可以获取到View的测量宽高了。但有时候View会进行多次测量,所以最好是在layout中获取测量宽高。
需要注意的是View的测量过程和Activity的生命周期是不同步的,我们不能在onCreate、onStart、onResume中获取测量宽高。建议在通过以下三种方法获取:
1.onWindowFocusChanged方法中获取
2.通过view.post(runnable)在run中获取
3.通过ViewTreeObserber获取

Layout过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup 的位置被确定后,它在onIayout中会遍历所有的子元素并调用其layout 方法,在layout方法中onLayout方法又会被调用。Layout 过程和measure过程相比就简单多了,layout 方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的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);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList listenersCopy =
                        (ArrayList)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;
    }

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

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

LinearLayout中onLayout 的实现逻辑和onMeasure 的实现逻辑类似,这里选择layoutVertical继续看。

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

这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用
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);
    }

getMeasureWidth(Height)和getWidth(Height)的区别

View 的getMeasuredWidth和getWidth这两个方法有什么区别, 至于getMeasuredHeight和getHeight的区别和前两者完全一样。 为了回答这个问题,首先,我们看一下getwidth和getHeight这两个方法的具体实现:

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

从getWidth和getHeight的源码再结合mLef、mRight、 mTop 和mBottom这四个变量的赋值过程来看,getWidth 方法的返回值刚好就是View的测量宽度,而getHeight方法的返回值也刚好就是View 的测量高度。经过上述分析,现在我们可以回答这个问题了:在.View的默认实现中, View的测量宽/高和最终宽/高是相等的,只不过测量宽高形成于View的measure过程,而最终宽/高形成于View的layout 过程,即两者的赋值时机不同,测量宽高的赋值时机稍微早- -些。因此,在日常开发中,我们可以认为View的测量宽/高就等于最终宽高,但当我们重写view的layout方法,修改其宽高,那么两个方法的结果就不一样了。

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

还有另一种情况,如果measure需要多次时,getWidth和getMeasureWidth也会不一致。

Draw过程

Draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
(1)绘制背景background .draw(canvas)。
(2)绘制自己(onDraw)。
(3)绘制children (dispatchDraw)。
(4)绘制装饰( onDrawForeground)。
(5) 绘制焦点相关(drawDefaultFocusHighlight/debugDrawFocus)
这一点通过draw方法的源码可以明显看出来,如下所示。

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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;

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

你可能感兴趣的:(Android View工作原理详解(二)—三大流程)