Android View的绘制流程三部曲

Posted yinhuanxu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View的绘制流程三部曲相关的知识,希望对你有一定的参考价值。

在刚开始学习Java的时候,我看的是Mars老师的视频。Mars老师说过的一句话让我印象很深刻:要有一颗面向对象的心。

如果我们用面向对象的思维方式来思考,就会觉的View的绘制机制是很合理,很科学的。我们要在一张纸上画一幅画,首先要测量一下这幅画有多大吧,然后确定在这张纸的哪个地方画会显得比较美观,最后才是用画笔工具将画绘制在纸上。
android中也是一样的。View的绘制流程主要是指measure,layout,draw这三步,即测量,布局,绘制。首先是要测量View的宽高,然后布局确定其在父容器中的位置坐标,最后才是绘制显示出来。那这篇博客就一起来探索View的绘制流程吧。

View的绘制流程从ViewRootImpl的performTraversals方法开始,在performTraversals方法中会调用performMeasure、performLayout、performDraw三个方法来遍历完成整棵视图树的绘制。

measure过程

MeasureSpec

performMeasure方法是这样被调用的:

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接收了两个参数,很好奇这两个参数是什么。看名字是“子View宽测量说明书”和“子View高测量说明书”?应该先来了解一下MeasureSpec。

MeasureSpec是一个32位的int值,高2位是specMode记录的是测量模式,低30位是specSize记录的是测量大小。

specMode有三种类型:

EXACTLY : 精确值模式,表示父视图希望子视图的大小应该是由specSize的值来决定的,这个时候View的最终大小就是specSize所记录的大小。对应于LayoutParams中的 match_parent和具体数值这两种模式。比如 android:layout_width=”match_parent”,android:layout_width=”50dp”

AT_MOST : 最大值模式,表示父容器指定了一个可用大小specSize,子视图最多只能是specSize中指定的大小,不能大于这个值。对应于LayoutParams中的 wrap_content的形式。

UNSPECIFIED :父容器不对View有任何限制,View想多大就多大,一般不会用到

MeasureSpec到底是用来干嘛的?
系统是通过View的MeasureSpec来确定View的测量宽高的

MeasureSpec是怎么来的?
对于普通的View来说,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同确定。对于顶级View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。

我们回到performMeasure方法,来看看传入的参数childWidthMeasureSpec和childHeightMeasureSpec,这两个MeasureSpec是顶级View的,它们由窗口的尺寸和其自身的LayoutParams共同确定。那它们又是怎么产生的?在ViewRootImpl的measureHierarchy方法中,有两行代码是这样的:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

getRootMeasureSpec方法获取到根View(DecorView)的MeasureSpec。传入的参数desiredWindowWidth和desiredWindowHeight是屏幕的尺寸。lp.width 和lp.height都是MATCH_PARENT。

那么探探getRootMeasureSpec方法,如下:

private static int getRootMeasureSpec(int windowSize, int rootDimension) 
    int measureSpec;
    switch (rootDimension) 

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    
    return measureSpec;

会转到MeasureSpec的makeMeasureSpec方法,而makeMeasureSpec方法就是将SpecSize和SpecMode包装成32位的int值。
那makeMeasureSpec方法是怎么组装MeasureSpec的呢?如下:

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) 
    if (sUseBrokenMakeMeasureSpec) 
        return size + mode;
     else 
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    

这时,根View的MeasureSpec就诞生了。它将参与构成子元素的MeasureSpec。

而对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同构成。我们知道刚才getRootMeasureSpec方法获取到的是顶级View的MeasureSpec,顶级View本身就是父容器。
那现在看看ViewGroup的measureChildWithMargins方法,这个方法是用来测量子View的。如下:

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

首先是调用子元素的getLayoutParams方法获取到子元素的LayoutParams,之后调用了getChildMeasureSpec方法来获取到子元素的MeasureSpec,可以看到传入了父元素的MeasureSpec。

getChildMeasureSpec方法很重要,能让我们了解子元素MeasureSpec的产生过程,如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) 
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

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

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) 
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         else if (childDimension == LayoutParams.MATCH_PARENT) 
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
         else if (childDimension == LayoutParams.WRAP_CONTENT) 
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) 
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         else if (childDimension == LayoutParams.MATCH_PARENT) 
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
         else if (childDimension == LayoutParams.WRAP_CONTENT) 
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) 
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         else if (childDimension == LayoutParams.MATCH_PARENT) 
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
         else if (childDimension == LayoutParams.WRAP_CONTENT) 
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        
        break;
    
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

经过了getChildMeasureSpec方法,子元素的MeasureSpec也诞生了。这个方法代码虽然长长的,但逻辑并不复杂,就是根据父容器的MeasureSpec和子元素的LayoutParams来组装子元素的MeasureSpec。所以说普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

那么现在已经搞定了MeasureSpec,跟进performMeasure方法看看到底View的测量过程是怎样的。

View的测量

performMeasure方法源码如下:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) 
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try 
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
     finally 
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    

转到了View的measure方法,如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) 

    //代码省略

    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    //代码省略
    if (forceLayout || needsLayout) 
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) 
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
         

        //代码省略

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension

可以看到View的measure方法是带final的,不允许子类重写。经过一系列的处理,会转到onMeasure方法,那就跟进View的onMeasure方法探探:

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

调用了setMeasuredDimension将测量的宽高设置进去,好像很简单的说。getDefaultSize方法用于获取测量宽高,源码如下:

public static int getDefaultSize(int size, int measureSpec) 
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) 
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    
    return result;

其实内部逻辑是很简单的,从measureSpec中取出specMode和specSize,然后就是AT_MOST和EXACTLY的情况下,都返回specSize,这个specSize就是测量的值了。

以上就是View的测量过程。

补充:对于TextView、Button、ImageView等,它们都是重写了onMeasure方法的,可以阅读一下它们的onMeasure方法源码

ViewGroup的测量

那么接下来是ViewGroup的测量过程,ViewGroup中是没有重写onMeasure方法的,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?
我们可以想一下的,怎么能定义出一个符合多种ViewGroup的onMeasure方法呢?很显然LinearLayout和RelativeLayout的onMeasure方法实现是不一样的。所以需要由子类去实现,这也是很合理的。

ViewGroup除了完成自身的测量,还会遍历子元素,如此循环完成整棵视图树的测量过程。在ViewGroup中定义了一个measureChildren方法去遍历子元素,如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) 
     final int size = mChildrenCount;
     final View[] children = mChildren;
     for (int i = 0; i < size; ++i) 
         final View child = children[i];
         if ((child.mViewFlags & VISIBILITY_MASK) != GONE) 
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
         
     

会转到measureChild方法中去测量子元素。

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) 
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

就这样,ViewGroup将measure过程传递到了子元素。如此反复完成整棵视图树的绘制。

以上就是ViewGroup的测量过程,至此,View的测量过程已经分析结束。当measure过程完成后,就可以调用getMeasuredWidth/getMeasuredHeight方法来获取测量宽高了。理解ViewGroup的测量,可以阅读下LinearLayout的onMeasure方法源码。

layout过程

在performLayout方法中转到layout方法来完成View布局过程。那就来看看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;

首先会调用setFrame将 l, t, r, b 四个参数传入,确定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;

        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;

        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;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

    //代码省略

    return changed;

可以看到,其实就是初始化了mLeft、mTop、mRight、mBottom。经过了setFrame方法后,View在父容器中的位置也就确定了。

眼尖的你发现了layout方法中,调用了 onLayout方法,这个onLayout方法是父容器用来确定子元素的位置的。你也应该猜到了onLayout方法内又会遍历子元素,然后调用子元素的layout来确定子元素在父容器中的位置。

那我们跟进去onLayout方法看看:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) 

咦?竟然是空的。其实onLayout方法和onMeasure方法相似,需要由子类去具体实现。
我们看看DecorView的onLayout方法,DecorView也是一个ViewGroup嘛。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) 
    super.onLayout(changed, left, top, right, bottom);

   //代码省略

转到了父类(FrameLayout)的onLayout方法,我们继续跟进去

protected void onLayout(boolean changed, int left, int top, int right, int bottom) 
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);

又转到了layoutChildren方法去布局子元素。layoutChildren方法如下:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) 
    final int count = getChildCount();

    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) 
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) 
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) 
                gravity = DEFAULT_CHILD_GRAVITY;
            

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) 
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) 
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            

            switch (verticalGravity) 
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        
    

呐,总之就是会遍历出子元素,然后调用子元素的layout方法,然后在子元素的layout方法中又会调用setFrame方法来确定其在父容器中的位置。如此反复完成视图树的布局过程。

以上就是layout过程。

draw过程

在performDraw方法中,会调用draw方法的重载,之后会转到draw(Canvas canvas)方法,如下:

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;
    

    //代码省略


我们关注1,3,4,6步。
第一步: drawBackground(canvas) 绘制背景
第三步: onDraw(canvas) 绘制自己,具体如何绘制?需要由子类具体实现,可以阅读下TextView的onDraw方法源码
第四步: dispatchDraw(canvas) 绘制子元素,既然是绘制子元素的话,那么ViewGroup实现了这个方法,来探探ViewGroup的dispatchDraw方法,如下:

protected void dispatchDraw(Canvas canvas) 

    //代码省略

    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;
            
        

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) 
            more |= drawChild(canvas, child, drawingTime);
        
    

    //代码省略

在dispatchDraw方法中会遍历子元素,转到drawChild方法,在drawChild方法中又会调用View的draw方法来完成子元素的绘制过程,如此循环完成整个视图树的绘制。
第六步:onDrawForeground(canvas) 绘制前景,ScroolBars。

以上就是draw过程。

至此,View的绘制流程已经全部分析完了。

以上是关于Android View的绘制流程三部曲的主要内容,如果未能解决你的问题,请参考以下文章

android view从无到有的过程

[Android FrameWork 6.0源码学习] View的重绘过程

Android面试收集录12 View测量布局及绘制原理

android基础-viewgroup的测量,布局,绘制

[译]Android view 测量布局和绘制的流程

反思|Android View机制设计与实现:测量流程