Android 浅谈View的绘制流程

ViewRoot

在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。
在Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程的源码如下:

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

View绘制的起点

View绘制的起点是以ViewRootImpl的performTraversals()方法被调用开始的。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。他经过measure layout draw三个过程才能最终将一个View绘制出来。
其大致流程见下图:

还有这开发艺术上的经典:

MeasureSpec

在具体讲解view绘制三大过程之前,我们先看看MeasureSpec这个概念。MeasureSpec代表一个32位的int值,高两位代表SpecMode,低两位代表SpecSize。SpecMode是指测量模式,SpecSize是指在某种测量模式下的规格大小。下面我们看看具体的代码(节省篇幅,源码中的注释已删除):

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

       
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

       
        public static final int AT_MOST     = 2 << MODE_SHIFT;

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

    
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

      
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

         
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

这里MeasureSpec通过将SpecMode、SpecSiza打包成一个int值,并且提供了获取SpecMode和SpecSize的方法,分别对应的getMode和getSize方法。可以看到一组SpecMode、SpecSize可以很容易打包成一个MeasureSpec,而一个MeasureSpec也可以很容易得到他的测量模式以及view的规格大小。

SpecMode

简单介绍下SpecMode,有三种,如下:

1. UNSPECIFIED

父布局不对View有任何限制,要多大有多大,这种情况下一般只使用于系统内部,表示一种测量状态

2. EXACTLY

父容器已经检测出View所需要的精确大小,这个时候View的最终的大小就是这个SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式

3. AT_MOST

父布局指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现。他对应于LayoutParams中的wrap_content.

Measure过程

measure过程分为View和ViewGroup的measure过程。如果是一个原始的view,那么通过measure方法就可以完成其测量过程。而ViewGroup的测量除了要完成自己的测量值外,还要遍历所有子View并调用他们的measure,各个元素再递归执行这个流程。

1.ViewGroup的Measure流程

Android系统的视图结构的设计也采用了组合模式,即View作为所有图形的基类,Viewgroup对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;
        //对所有子View进行遍历
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            //当View不为GONE状态时,进行测量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

这个方法就实现了遍历子View的大小,再看看上面代码最后调用的measureChild方法:

  /**
     * 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();
//可以看出子View的MeasureSpec由父View及其自己的MeasureSpec组成,而且还加入了padding值,这是因为
//要考虑到父View被占的大小,这样最终的大小才组成了子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
//这里又调用了子View的measure方法,使子view继续遍历测量它的子view,这样就实现了遍历测量了
//整个ViewGroup里的所有View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这里有出现了两个新方法,不急我们一个个的来看看,先看getChildrenMeasureSpec方法,刚刚我们知道了子View的MeasureSpec由父View及其自己的MeasureSpec组成,那到底是根据是很么样的规则决定的呢。我们一起看看这个方法的源码:

   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
//这里表示父View可用大小为父View的尺寸减去padding值(父View被占用的大小)的结果
        int size = Math.max(0, specSize - padding);
        
        int resultSize = 0;
        int resultMode = 0;
//这里的specMode是父View的,也就是说先根据父View的测量模式再对应子View的测量模式决定
//子View的specMode和specSize。
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
            //如果父View是EXACTLY模式,而子View的大小设置值不小于0,那么子View的specSize
            //就为子View的大小设置值,specMode就为EXACTLY模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果父View是EXACTLY模式,而子View设置的是MACTH_PARENT,那么子View的specSize就为父
             //View的值,specMode设置为EXACTLY模式
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果父View是EXACTLY模式,而子View设置的是WRAP_CONTENT,那么子View的specSize就为父
            //View的值,specMode设置为AT_MOST模式
                // 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
    //最后将子view的measureSpec打包完成
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

看完这个方法,再结合前面所讲,我们已经有了个明确的意识,那就是ViewGroup遍历测量子View时,子View的大小,也就是测量结果不仅只是子View自身决定,而是由父容器的MeasureSpec和子View的LayoutParams(当然还要考虑到View的margin和padding值相关)共同决定子View的MeasureSpec。上面代码给我们展示了是如何共同决定的,如果还不直观,我们在看看这张图:

对于DecorView来说,它的MeasureSpec与普通View不太相同。它由窗口的尺寸和其自身的LayoutParams共同决定。对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下代码:

       if (!goodMeasure) {
       //这里展示了DecorView的MeasureSpec的创建过程
       //desiredWindowWidth,desiredWindowHeight为屏幕的宽高
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }

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

这里的规则比较简单了:根据他的LayoutParams中的参数来划分:

  1. LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的尺寸
  2. LayoutParams.WRAP_CONTENT:最大模式,大小不定,都不能超过窗口的尺寸
  3. 固定大小:精确模式,大小就为指定大小

到这里我们就看完了ViewGroup的measureChildren方法,measureChild方法,以及getChildMeasureSpec方法,而这系列的方法简单的来说就是实现了ViewGruop遍历其下所有View并生成对应View的MeasureSpec这个过程。那我们就想知道最后每一个View的测量过程如何实现,那就看第二小点,View的Measure过程。

2、View的Measure过程

View的masure过程由ViewGroup传递,这个方法叫measureChildWithMargins方法,顾名思义,这个方法与margin值有关。我们还要看看这个方法:

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

这里与measureChild方法不同的只是加入了margin值而已,其他的都差不多,就不多看了。这里调用了measure方法。View的measure方法不能重写,并且View中会执行View的onMeasure方法,所以在写自定义view时一定要重写onMeasure方法。我们直接来看onMeasure方法的源码:

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

这段代码很短,但有好几个方法,setMeasureDinmension方法,getDefaultSize方法以及getSuggestedMinimumWidth方法。我们用一张图更直观的看看他的原理:

首先来看setMeasureDinmension方法,其实就是设置View的长宽测量值,源码如下:

 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;
        }
        //调用方法将测量的长宽设置为view的长宽
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

这里又调用了setMeasuredDimensionRaw方法:

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

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

显然,setMeasureDinmension方法并不是重点,这个方法就是将测量的长宽设置为view的长宽,我们再看看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.
     * 实用程序返回默认大小。使用所提供的大小,如果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;
        }
        //返回一个specSize值,这个值就是view的测量大小,他真正的大小需要在Layout阶段确定,但一般这两个值相等
        return result;
    }

这里在MATCH_PARENT和WRAP_CONTANT模式下,getDefaultSize返回的就是specSize的值,而在UNSPECIFIED模式下,宽高分别返回了getSuggestMinimumWidth和getSuggestMinimumHeight方法的返回值。我们看看这两个方法的源码:

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

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

以getSuggestedMinimumWidth方法为例,从代码中可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而这个mMinWidth值就是android:minwidth属性所指的值。如果这个属性没有指定值,那默认值为0;如果View有背景,则View的宽度为个mMinWidth和mBackground.getMinimumWidth()的返回值的最大值。这里的mBackground.getMinimumWidth()是Drawable的getMinimumWidth方法里的,我们看看源码:

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

这段代码返回的就是Drawable的原始高度,如果有原始高度就返回值,否则返回0,比如BitmapDrawable就有原始高度,而ShapeDrawable就没有。
最后总结一下getDefaultSize方法,View的高/宽由specSize决定,而直接继承View的自定义空白控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则其wrap_content和match_parent是一样的效果。至于原因,前面已经讲过了,就不在赘述了。我们这里提供了这个问题的解决办法:

private int mMinWidth = 250; // 指定默认最小宽度
private int mMinHeight = 250; // 指定默认最小高度

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, 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(mMinWidth, mMinHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mMinWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mMinHeight);
    }
}

上面这段代码中,我们指定了一个默认的宽高大小:mMinWidth、 mMinHeight,并在wrap_content模式下时设置此默认大小值。而其他模式就跟原来一样的。
到这里view的measure过程就简单介绍完了,在measure完成之后,我们可以通过getMeasuredWidth/Heigth方法获取到View的测量宽/高,但在某些情况下,获得的值并不准确,所以建议在onLayout方法中去获取View的最终宽/高。那么我们来看看Layout过程。

Layout过程

测量结束后,视图的大小就已经测量好了,接下来就是 Layout 布局的过程。上文说过 ViewRoot 的 performTraversals 方法会在 measure 结束后,执行 performLayout 方法,performLayout 方法则会调用 layout 方法开始布局,代码如下

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }
 try {
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    //...省略代码
    } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;

我们看看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;
        }
//记录下View原始位置
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
//通过setFrame方法设置子元素的四个顶点的位置
//返回布尔值判断View布局是否改变
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果View位置改变,调用onLayout方法
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout方法首先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mTop,mRight,mBottom这四个值,View的四个顶点一确定,那么它在父容器里的位置也就确定了,关键源码如下:

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

接着又调用了onLayout方法,这个方法确定了子元素的位置,onLayout 源码如下:

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

看到这,是不是觉得不对,为什么是个空方法,没错,就是一个空方法,因为 onLayout 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置,我们继续看 ViewGroup 中的 onLayout 方法

@Override  
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

,ViewGroup 中的 onLayout 方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。接下来我们看看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方法看看,主要源码:

  void layoutVertical(int left, int top, int right, int bottom) {
        ···
        //childTop为View到Top的高度
        //循环遍历子View
        for (int i = 0; i < count; i++) {
        //获取指定View
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
            //如果View可见,获取子元素的测量宽高
            //在这里可以看出setChilFrame方法传入的参数实际上就是子元素的测量宽高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
            //获取子元素的LayoutParams参数
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

              ···

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                //设置子View位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                        //重新计算View到top的位置
                        //下一个子View的top位置就会相应的增加
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

上面代码主要完成了遍历所有子元素并调用了setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这样后面的元素就会放在更靠下的位置,这也刚好符合垂直方向线性布局的特点。再看setChildFrame方法,代码如下:

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

可以看到这个方法就是调用了子元素的layout方法,这样子元素在确定了自己的位置后,又会调用onLayout方法继续往下确定子元素的位置。最后整个View树的全部元素的位置就都确定了。

Draw过程

相比前面两个过程,Draw过程已经简单了许多了,它主要有如下几步:

  1. 绘制背景background。draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawForeground)

前面说过Draw过程通过performDraw方法发现它调用了draw方法,所以我们看看到底draw方法是如何实现的:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

      

        // 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 绘制自己 调用onDraw方法
            //onDraw是一个空方法,这是因为没个视图的内容部分都不太相同
            //自定义View就必须重写这个方法来实现View的绘制
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children 分发绘制子元素
            //ViewGroup的dispatchDraw方法有具体的绘制逻辑
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            //绘制装饰
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

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

       ···

    }

View的绘制过程的传递是在dispatchDraw方法中实现的,它会遍历所有子元素,然后调用draw方法,这样view的draw事件就一层一层的传递下去了。看看ViewGroup中的dispatchDraw方法的代码:

 @Override
    protected void dispatchDraw(Canvas canvas) {
       boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
            ···
                  // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
                //对子元素进行遍历,同时调用了drawChild方法
        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;
                }
            }
            ···
}

在看看drawChild方法的源码:

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

显然,View的draw过程就要完成了,这里又调用了draw方法实现了每个子元素的绘制。
也就是说,到了dispatchDraw方法这里,Draw过程就完成了。View的绘制过程也就全部完成了。

站在巨人的肩膀上

本文参考了《Android开发艺术探索》

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

推荐阅读更多精彩内容