Android View的测量、布局、绘制流程源码分析及自定义View实例演示

在Android知识体系中,Android系统提供了一个GUI库,里面有很多原生控件,但是很多时候我们并不满足于系统提供的原生控件,那么怎么才能做出所需要的控件?那就要自定义View,在实际开发中我们仅仅了解常用原生控件的使用方法是无法作出需要的复杂的自定义View的。因此为了做出需要的复杂的自定义View,还需要掌握View的测量过程 (measure)、布局过程(layout)和绘制过程(draw)。下面先来分析View的测量过程 (measure)、布局过程(layout)和绘制过程(draw)。

预备知识

  1. 应用程序窗口的视图对象(DecorView类型的对象)的三大流程
    下面提到的顶层视图代表应用程序窗口的视图对象(DecorView类型的对象),下面提到的ViewRoot对于ViewRootImpl类
    顶层视图及其所关联的ViewRoot对象的创建过程,如下图所示(参考文档1):

    上图中第9步获取到的就是顶层视图decor,第11、12、13步就是将decor传递给ViewRoot,这样ViewRoot就和DecorView建立了关联。在第13步中,ViewRoot类的成员函数setView会调用ViewRoot类的另外一个成员函数requestLayout来请求 对顶层视图decor 作第一次布局以及显示。接下来,我们就从ViewRoot类的成员函数requestLayout开始,分析顶层视图decor的三大流程,如下图所示:

    上图中的第5步会调用ViewRootImpl类的performTraversals方法,performTraversals方法会依次调用performMeasure方法、performLayout方法和performDram方法来完成顶层视图decor的测量过程 (measure)、布局过程(layout)和绘制过程(draw)。

View的测量过程 (measure)

上图的第9步会遍历每一个子View,被调用子View的measure方法(即第10步),继而开始进行子View的测量过程 (measure)。ViewGroup类型的View和非ViewGroup类型的View的测量过程是不同的,非ViewGroup类型的View通过onMeasure方法就完成了其测量过程,而ViewGroup类型的View除了通过onMeasure方法就完成自身的测量过程外,还要在onMeasure方法中完成遍历子View的measure方法,各个子View再去递归执行这个流程。

  1. 非ViewGroup类型的View的测量过程
    先通过如下的时序图,整体的看一下测量过程:


    View的测量时序图.jpg

    对于上面的步骤进行解析一下,第1步执行View类中的measure方法,该方法是一个final方法,这就意味着子类不能从写该方法,measure方法会调用View类的onMeasure方法,onMeasure方法的实现代码如下所示:

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

从上面的代码就对应上图中3、4、5、6、7步,先来看第3步对应的View类的getSuggestedMinimumWidth方法的源码:

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

从getSuggestedMinimumWidth的代码可以看出,当View没有设置背景,那么getSuggestedMinimumWidth方法的返回值为mMinWidth,而mMinWidth对应于android: minWidth属性指定的值,即getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值,如果没有设置android: minWidth属性,则mMinWidth默认为0;如果View设置了背景,则getSuggestedMinimumWidth方法的返回值为max(mMinWidth, mBackground.getMinimumWidth()),下面先来看看Drawable类中getMinimumWidth方法的源码:

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

有上面的代码可知getMinimumWidth返回的是View的背景的原始宽度,如果View的背景没有原始宽度,就返回0。
现在来总结一下getSuggestedMinimumWidth方法的逻辑,当View没有设置背景时,getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值,这个值可以为0;当View设置了背景时,getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值与View的背景的最小宽度中的最大值。
现在我们来看一下最关键的View类的getDefaultSize方法的源代码(对应第4步):

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

上面的逻辑很简单,对于MeasureSpec.AT_MOST和MeasureSpec.EXACTLY测量模式,getDefaultSize直接返回测量后的值(即父View通过measure方法传递过来的测量值,也说明了下面注意事项中的第一条);对于MeasureSpec.UNSPECIFIED测量模式,一般用于系统内部的测量过程,getDefaultSize返回值为getSuggestedMinimumWidth方法的返回值。
对于第5、6步与3、4步类似,这里就不再缀续了。
第7步中View类的setMeasuredDimension方法调用了第8步中View类的setMeasuredDimensionRaw方法,setMeasuredDimensionRaw方法的源码:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

有上面的代码可知,View测量后的宽高被保存到View类的成员变量mMeasuredWidth和mMeasuredHeight中了,通过View类的getMeasuredWidth方法和getMeasuredHeight方法获取的就是mMeasuredWidth和mMeasuredHeight的值,需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在onMeasure方法中拿到的测量宽高很可能是不准确的,一个好的习惯是在onLayout方法中去获取View最终的测量宽高。上面只是说在自定义View中什么时机获取最终的测量宽高,那在Activity中什么时机获取View的测量宽高呢?又如下四种方法(具体描述请参考 Android开发艺术探索 的190页):

1 在Activity/View#onWindowFocusChanged方法中获取
2 在Activity中的onStart方法中执行View.post获取
3 通过ViewTreeObserver获取
4 通过手动执行View.measure获取

有如下几点需要注意:
1>直接继承View的自定义控件需要重写onMeasure方法并且设置wrap_content时的自身大小,否者在布局中使用wrap_content就相当于使用math_parent,具体原因上面已经说明。
2> 在自定义View时可以通过重写onMeasure方法设置View测量大小,这样的话你就抛弃了父容器通过measure方法传进来建议测量值MeasureSpec。

  1. ViewGroup类型的View的测量过程
    先通过如下的时序图,整体的看一下测量过程:


    ViewGroup的测量时序图.jpg

    ViewGroup并没有定义其自身测量的具体过程(即没有onMeasure方法),这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,所以上面展示了LinearLayout测量过程图
    对于上面的步骤进行解析一下,第1步执行View类中的measure方法,该方法是一个final方法,这就意味着子类不能从写该方法,measure方法会调用LinearLayout类的onMeasure方法,onMeasure方法的实现代码如下所示:

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

我们现在只分析当LinearLayout的方向是垂直方向的情况,此时会执行LinearLayout类的measureVertical方法,代码如下(由于measureVertical方法的代码比较长,下面只展示我们关心的逻辑代码):

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    // 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).
......
    measureChildBeforeLayout(
           child, i, widthMeasureSpec, 0, heightMeasureSpec,
           totalWeight == 0 ? mTotalLength : 0);
    
    if (oldHeight != Integer.MIN_VALUE) {
       lp.height = oldHeight;
    }
    
    final int childHeight = child.getMeasuredHeight();
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
           lp.bottomMargin + getNextLocationOffset(child));
......
}
......
// 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;
......
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);
.....

由上半部分的代码可知LinearLayout类的measureVertical方法会遍历每一个子元素并且执行LinearLayout类的measureChildBeforeLayout方法对子元素进行测量,LinearLayout类的measureChildBeforeLayout方法内部会执行子元素的measure方法。在代码中,变量mTotalLength会是用来存放LinearLayout在竖直方向上的当前高度,每遍历一个子元素,mTotalLength就会增加,增加的部分主要包括子元素自身的高度、子元素在竖直方向上的margin。当测量完所有子元素时,LinearLayout会根据子元素的情况测量自身的大小,针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向上的测量过程和View有所不同,具体来说是指,如果它的布局中高度采用的是math_content或者具体数值,那么它的测量过程与View一致,如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过父容器的剩余空间,这个过程对应与resolveSizeAndState的源码:

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

下面我们来看一看LinearLayout类的measureChildBeforeLayout方法是如何对子元素进行测量,该方法的第第4个和第6个参数分别代表在水平方向和垂直方向上LinearLayout已经被其他子元素占据的长度,measureChildBeforeLayout的源码如下:

void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}

LinearLayout类的measureChildBeforeLayout方法会调用ViewGroup类的
measureChildWithMargins方法,measureChildWithMargins方法的源码如下:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

ViewGroup类的measureChildWithMargins方法会调用子元素的measure方法对子元素进行测量,在对子元素测量之前先会通过调用ViewGroup类的getChildMeasureSpec方法得到子元素宽高的MeasureSpec,从传给ViewGroup类的getChildMeasureSpec方法的前二个参数可知,子元素MeasureSpec的创建与父容器的MeasureSpec、父容器的padding、子元素的margin和兄弟元素占用的长度有关。ViewGroup类的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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

ViewGroup类的getChildMeasureSpec方法的逻辑可以通过下表来说明,注意,表中的parentSize是指父容器目前可使用的大小(参考Android开发艺术探索182页):

childLayoutParams/parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY/childSize EXACTLY/childSize EXACTLY/childSize
MATCH_PARENT EXACTLY/parentSize AT_MOST/parentSize UNSPECIFIED/0
WRAP_CONTENT AT_MOST/parentSize AT_MOST/parentSize UNSPECIFIED/0

ViewGroup类的getChildMeasureSpec方法返回子元素宽高的MeasureSpec,然后将子元素宽高的MeasureSpec作为measure方法的参数。

到此为止,非ViewGroup类型的View的测量过程和ViewGroup类型的View的测量过程已经分析完毕,进行如下总结:
1> 父View会遍历测量每一个子View(通常使用ViewGroup类的measureChildWithMargins方法),然后调用子View的measure方法并且将测量后的宽高作为measure方法的参数,但是这只是父View的建议值,子View可以通过继承onMeasure来改变测量值。
2> 非ViewGroup类型的View自身的测量是在非ViewGroup类型的View的onMeasure方法中进行测量的
3> ViewGroup类型的View自身的测量是在ViewGroup类型的View的onMeasure方法中进行测量的
4>直接继承ViewGroup的自定义控件需要重写onMeasure方法并且设置wrap_content时的自身大小,否者在布局中使用wrap_content就相当于使用math_parent,具体原因通过上面的表格可以说明。

View的布局过程(layout)

decor的三大流程图的第16步会遍历每一个子元素,并且调用子元素的layout方法,继而开始进行子元素的布局过程。layout过程比measure过程简单多了,layout方法用来确定View本身的位置,而onLayout方法用来确定所有子元素的位置。ViewGroup类型的View和非ViewGroup类型的View的布局过程是不同的,非ViewGroup类型的View通过layout方法就完成了其布局过程,而ViewGroup类型的View除了通过layout方法就完成自身的布局过程外,还要调用onLayout方法去遍历子元素并且调用子元素的layout方法,各个子View再去递归执行这个流程。

  1. 非ViewGroup类型的View的布局过程
    先通过如下的时序图,整体的看一下布局过程:


    View的布局时序图.jpg

    对上面的时序图进行一下解析,第1步执行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<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;
}

如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),所以无论如何都会执行setFrame()方法;第2步layout方法调用View类的setFrame方法,部分我们感兴趣的源码如下:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

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

        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
    }
    return changed;
}

由上面的源码可知,setFrame方法是用来设定View的四个顶点的位置,即初始化mLeft、mTop、mRight、mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;第3步layout方法接着调用View类的onLayout方法,这个方法的作用是用来确定子元素的位置,由于非ViewGroup类型的View没有子元素,所以View类的onLayout方法为空。

  1. ViewGroup类型的View的布局过程
    先通过如下的时序图,整体的看一下布局过程:


    ViewGroup的布局时序图.jpg

    上面其实是LinearLayout的布局时序图,因为ViewGroup的onLayout方法是抽象方法,所以就选择了ViewGroup的子类LinearLayout进行分析。对上面的时序图进行一下解析,第1步执行ViewGroup类的layout方法,该方法是一个final方法,即子类无法重写该方法,源代码如下:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

第2步ViewGroup类的layout方法会调用View类的layout方法,第3步View类的layout方法调用View类的setFrame方法,这两步与上面讨论非ViewGroup类型的View的布局过程的第1、2步相同,这里就不在赘叙,直接看第4步View类的layout方法调用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);
    }
}

我们现在只分析当LinearLayout的方向是垂直方向的情况,此时会执行LinearLayout类的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);
        }
    }
}

可以看到LinearLayout类的onLayout方法会遍历每一个子元素,然后调用LinearLayout类的setChildFrame方法,setChildFrame方法会调用子元素的layout方法来对子元素进行布局,setChildFrame方法的源码如下:

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

View的绘制过程(draw)

decor的三大流程图的第23步会遍历每一个子View,并且调用子元素的draw方法,继而开始进行子View的绘制过程。先通过如下的时序图,整体的看一下绘制过程:


ViewGroup的绘制时序图.jpg

上面其实是LinearLayout的绘制时序图,因为View的onDraw方法是空方法,所以就选择了ViewGroup的子类LinearLayout进行分析。
LinearLayout的绘制过程遵循如下几步:
1> 绘制背景
2> 绘制自己(绘制分割线)
3> 绘制子View(dispatchDraw)
4> 绘制前景
Android中是通过View类的draw方法来实现上面的4步,源码如下所示:

/**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called.  When implementing a view, implement
 * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 * If you do need to override this method, call the superclass version.
 *
 * @param canvas The Canvas to which the View is rendered.
 */
@CallSuper
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);

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

        // we're done...
        return;
    }
.....
}

从这个方法的注释可以知道,当自定义View并且需要绘制时,应该重写View类的onDraw方法而不要重写View类的draw方法,如果你需要重写draw方法,必须在重写时调用父类的draw方法。上面的代码很明显的验证了View绘制过程的4步。由于View类无法确定自己是否有子元素,所以View类的dispatchDraw方法是空方法,那么我们就来看看ViewGroup类的dispatchDraw方法的源码(由于该方法的源码太长了,因此我只展示我们感兴趣的部分代码):

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    boolean more = false;
    final long drawingTime = getDrawingTime();

    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
            ? null : buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
......
}

ViewGroup类的dispatchDraw方法会遍历每一个子元素,然后调用ViewGroup类的drawChild方法对子元素进行绘制,ViewGroup类的drawChild方法源码如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

与View生命周期相关的常用的回调方法

onFocusChanged(boolean, int, android.graphics.Rect):该方法在当前View获得或失去焦点时被回调。
onWindowFocusChanged(boolean):该方法在包含当前View的window获得或失去焦点时被回调。
onAttachedToWindow():该方法在当前View被附到一个window上时被回调。
onDetachedFromWindow():该方法在当前View从一个window上分离时被回调。
onVisibilityChanged(View, int):该方法在当前View或其祖先的可见性改变时被调用。
onWindowVisibilityChanged(int):该方法在包含当前View的window可见性改变时被回调。

自定义View实例

  1. 自定义View的分类
    自定义View的分类标准不唯一,而我把自定义View分为3类
    1> 通过继承View或者ViewGroup实现自定义View
    2> 通过继承已有的控件实现自定义View
    3> 通过组合实现自定义View
    我在下面只针对1>来实现自定义View,因为2>和3>相对于1>就比较简单了。
  2. 通过继承View实现环状进度条
    首先展示一下效果图:


    环状进度条

    下面就来分析一下实现代码:
    根据上面对非ViewGrop类型View三大流程的分析,第一步就是测量,
    由于是继承View类的,因此如果想要支持wrap_content属性,就必须重写onMeasure方法,如下所示(可以当做模板代码):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    } else {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

第二步就是进行布局,由于非ViewGrop类型View自身的布局在View类的layout方法中已经实现,而onLayout方法是用来对子View进行布局的,所以对于非ViewGrop类型View就不用考虑布局的实现。
第三步就是进行绘制,由于非ViewGrop类型View没有子View,所以不用考虑对子View的绘制,因此只要重写View类的onDraw方法对自身进行绘制即可,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawArc(new RectF(getPaddingLeft(), getPaddingTop(), mWidth - getPaddingRight(), mHeight - getPaddingBottom()), 0, sweepValue, false, paint);
}

从上面的代码中可以看出,如果不在onDraw方法中处理padding,那么padding属性无法起作用。

  1. 通过继承ViewGroup实现流式布局(FlowLayout)
    首先展示一下效果图:


    流式布局

    下面就来分析一下实现代码:
    根据上面对ViewGrop类型View三大流程的分析,第一步就是测量,
    由于是继承ViewGrop类的,因此如果想要支持wrap_content属性,就必须重写onMeasure方法,代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    mLineWidths.clear();
    mLineHeights.clear();
    mLineViewNums.clear();
    int width = 0;
    int lineWidth = 0;
    int height = 0;
    int lineHeight = 0;
    int lineViewNum = 0;

    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);

        if (View.GONE == childView.getVisibility()) {
            if (i == childCount - 1) {
                lineViewNum++;
                mLineViewNums.add(lineViewNum);
                mLineWidths.add(lineWidth);
                width = Math.max(width, lineWidth);
                mLineHeights.add(lineHeight);
                height += lineHeight;
            }
            continue;
        }

        MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
        measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        if (lineWidth + childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin > widthSpecSize - getPaddingLeft() - getPaddingRight()) {
            mLineViewNums.add(lineViewNum);
            lineViewNum = 1;
            mLineWidths.add(lineWidth);
            lineWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            mLineHeights.add(lineHeight);
            height += lineHeight;
            lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        } else {
            lineViewNum++;
            lineWidth += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            width = Math.max(width, lineWidth);
            lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        }
    }
    mLineViewNums.add(lineViewNum);
    mLineWidths.add(lineWidth);
    mLineHeights.add(lineHeight);
    height += lineHeight;

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, height + getPaddingTop() + getPaddingBottom());
    } else {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

上面代码的逻辑:遍历每一个子元素,然后通过measureChildWithMargins方法对子元素进行测量,注意第3个和第5个参数必须是0,因为我是想在父元素所占有的空间中为子元素进行测量,在遍历每个子元素的过程中,记录每一行的最终宽度、最终高度和每一行的子元素个数。
第二步就是进行布局,由于ViewGrop类型View自身的布局在ViewGrop类的layout方法中已经实现,ViewGrop类的layout方法会调用ViewGrop类的onLayout方法,由于ViewGrop类的onLayout方法是抽象的,所以必须实现onLayout方法并且实现对子View的布局,代码如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int lineNum = mLineWidths.size();
    int paddingTop = getPaddingTop();
    int startIndex = 0;
    int endIndex = 0;
    for (int line = 0; line < lineNum; line++) {
        int paddingLeft = 0;
        int currentLineWidth = mLineWidths.get(line);
        switch (mGravity) {
            case LEFT:
                paddingLeft = getPaddingLeft();
                break;
            case CENTER:
                paddingLeft = (getWidth() - currentLineWidth)/2;
                break;
            case RIGHT:
                paddingLeft = getWidth() - currentLineWidth - getPaddingRight();
                break;
        }

        endIndex += mLineViewNums.get(line);
        for (; startIndex < endIndex; startIndex++) {
            View childView = getChildAt(startIndex);

            if (View.GONE == childView.getVisibility()) {
                continue;
            }

            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            int lc = paddingLeft + lp.leftMargin;
            int tc = paddingTop + lp.topMargin;
            int rc = childView.getMeasuredWidth() + lc;
            int bc = childView.getMeasuredHeight() + tc;
            childView.layout(lc, tc, rc, bc);
            paddingLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        }
        paddingTop += mLineHeights.get(line);
    }
}

上面代码的逻辑:逐行遍历每一个子View并且计算出子View的左上角和右下角的坐标,然后调用子View的layout方法对子View进行布局。
第三步就是进行绘制,由于我现在设计的流式布局不需要对自己进行绘制,所以不用考虑绘制。

参考文档

  1. Android应用程序窗口(Activity)的视图对象(View)的创建过程分析
  2. Android开发艺术探索
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容