详解View的绘制流程

来源:互联网 发布:淘宝是网站吗 编辑:程序博客网 时间:2024/06/08 07:35

前言:相信很多人无论是面试时还是开发中都遇到过需要弄清楚View的绘制流程的情况,而不深入View的源码是很难彻底搞清楚View的绘制的。本文是读了《Android开发艺术探索》这本书的View的工作原理这章加上笔者翻阅View的源码总结而成的,在此向作者表示崇高的敬意并向大家极力推荐这本书----------此书也是有初级Android开发工程师走向中高级Android开发工程师必读。


      整个View树的绘制流程是从ViewRoot.java类performTraversals()方法开始的,期具体的流程如下图所示:




       

那下面咱们就来认识一下View绘制的三部曲(measure,layout,draw)。

一、measure过程对整个view树的所有控件的宽高进行计算。

       1、measure的开始:measure是从ViewRoot的host.measure()方法开始的,内部调的是View的measure(int widthMeasureSpec, int heightMeasureSpec)方法。我们可以看到View的measure方法的参数比较特别,那下面咱们就去仔细了解一下该参数。

       2、MeasureSpec:这个参数是一个32位的int值,表示父控件对子View本身的测量宽高的期望。它的前两位表示测量模式SpecModel,后30位表示测量大小SpecSize。

                 SpecModel:(1)EXACTLY,精确模式。若layout.xml文件中宽高属性填写match_parent或者200px等精确值时,肯定是表示使用精确测量模式。可能有的人会烦嘀咕:“你说填写精确的如200px我知道是精确测量模式,但是为什么match_parent也是精确测量模式呢?”。这个显而易见,父控件的大小是确定的,你match_parent那肯定就是精确的测量模式。

                                       (2)AT_MOST,最大模式。若layout.xml文件中宽高属性填写wrap_content时表示使用最大测量模式,控件的大小随着其内容或者其内部的子控件的大小变化而变化,但是他的大小不能超过父控件规定的最大大小。

                                       (3)UNSPECIFIED,这个属性简直是个奇葩的存在。它表示的测量模式是无限大,就是你想要多大就多大,我们只在绘制特定情况的自定义view才用得到此模式。

      3、需要重写的方法:下面一段是View的measure方法的源码

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {             ..........        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),将父控件对宽高的期望值传入进去。                onMeasure(widthMeasureSpec, heightMeasureSpec);                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            } else {                long value = mMeasureCache.valueAt(cacheIndex);                // Casting a long to int drops the high 32 bits, no mask needed                setMeasuredDimensionRaw((int) (value >> 32), (int) value);                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            }            // flag not set, setMeasuredDimension() was not invoked, we raise            // an exception to warn the developer            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {                throw new IllegalStateException("View with id " + getId() + ": "                        + getClass().getName() + "#onMeasure() did not set the"                        + " measured dimension by calling"                        + " setMeasuredDimension()");            }            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;        }        mOldWidthMeasureSpec = widthMeasureSpec;        mOldHeightMeasureSpec = heightMeasureSpec;        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension    }



从上述源码我添加注释的地方我们可以看出,首先measure方法是final类型的,这也表明子类不能重写它。其次它里面调用的实际上是onMeasure方法,并将参数直接传给onMeasure方法。所以我们要对控件进行测量,实际上要重写的方法是onMeasure方法。

      4、onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法:我们还是从源码的角度入手,先看看onMeasure方法的源码

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

 该方法非常简洁,里面就调用了一个setMeasuredDimension方法,那我们可以肯定的就是setMeasuredDimension方法里面做的事肯定是做的真正设置view的测量宽高的方法,也是这个方法真正代表测量结束那么我们现在需要看的就是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;    }
我们可以看出,该方法传入的两个参数分别是默认的大小size以及父控件的期望measureSpec。该方法的内部实现也很简单,先通过
MeasureSpec.getMode(measureSpec)
MeasureSpec.getSize(measureSpec)

由measureSpec得到测量模式和测量大小两部分。

对于UNSPECIFIED测量模式,直接将size作为结果返回。

对于EXACTLY和AT_MOST这两种测量模式,则是将测量大小specSize作为结果返回。

     5、自定义View需要重写onMeasure方法吗?

           很明显,通常情况下我们在写直接继承View的自定义控件时并不需要重写onMeasure方法,但是当你要使用wrap_content属性是就必须要重写onMeasure方法。为什么呢?

           因为View默认的onMeasure方法只支持EXACTLY测量模式,如果不重写onMeasure方法那么你使用wrap_content属性的时候它所表现出来的效果和match_parent是一样的也是填充父控件。其实也很容易理解,默认的onMeasure方法怎么知道你包裹内容到底需要多大呢?是吧。所以要使用wrap_content属性的话就必须重写onMeasure方法。那么问题来了,我们怎么重写呢?

          其实很简单,写个示例代码大家就明白了。

//mWidth和mHeight表示自己设定的默认宽高。<pre name="code" class="java">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {            int widthSpecModel = MeasureSpec.getModel(widthMeasureSpec);            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);            int heightSpecModel = MeasureSpec.getModel(heightMeasureSpec);            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);            mWidth = Math.min(mWidth, widthSpecSize)            mHeight = Math.min(mHeight, heightSpecSize)            if (widthSpecModel == MeasureSpec.AT_MOST && heightSpecModel == MeasureSpec.AT_MOST) {                setMeasureDimension(mWidth, mHeight);            } else if (widthSpecModel == MeasureSpec.AT_MOST) {                setMeasureDimension(mWidth, heightSpecSize);            } else if (heightSpecModel == MeasureSpec.AT_MOST) {                setMeasureDimension(widthSpecSize, mHeight);            }        }

   6、到此,针对View的测量基本上就完成了,测量完成之后我们能得到getMeasuredWidth()和getMeasuredHeight()的值。这两个方法内部的实现非常简单

<span style="font-size:12px;">public final int getMeasuredHeightAndState() {        return mMeasuredHeight;    }</span>
直接返回的就是mMeasuredHeight,那我们就要找到mMeasuredHeight是在哪里赋值的

<span style="font-size:12px;">private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {        mMeasuredWidth = measuredWidth;        mMeasuredHeight = measuredHeight;        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;    }</span>
我们可以看到在setMeasuredDimensionRaw方法中我们对mMeasuredWidth进行了赋值,而通过源码我们可以发现setMeasuredDimensionRaw方法实际上就是在setMeasuredDimension这个方法中调用的。

<span style="font-size:12px;">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);    }</span>
所以我们getMeasuredWidth()和getMeasuredHeight()获取到的测量宽高实际上就是咱们setMeasuredDimension方法中设置的测量宽高。

      7、继承ViewGroup时是否需要重写onMeasure方法。上面我们讲完了继承View的onMeasure方法,那我们现在来探讨一下继承ViewGroup的onMeasure方法。

           对于ViewGroup除了要完成自己的measure过程之外还要遍历所有的子view对孩子进行测量。ViewGroup没有重写View的onMeasure方法,但它定义了measureChildren方法,该方法会遍历所有的子view并对调用measureChild方法对其进行测量。

<span style="font-size:12px;">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);            }        }    }</span>
       所以咱们在写继承ViewGroup的自定义控件时一定需要重写onMeasure方法对其子View进行测量,要不然子view会因为没有宽高而无法显示。


二、layout过程,该过程的作用是ViewGroup用来将子View放在合适的位置上。当ViewGroup的位置确定后,在onLayout中它将遍历所有子View,并调用它们的layout方法进行子View的布局,在子View的layout方法中会调用onLayout方法。

<span style="font-size:12px;">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方法。            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;    }</span>
在该方法中,先通过setFrame方法先确定View的左上右下四个点的坐标,也就是确定其位置,然后再调用onLayout方法来确定子View的位置。所以我们在写继承View的自定义控件时一般是不重写onLayout方法的,但是当我们写继承ViewGroup的自定义控件时一定要重写onLayout方法来布局子View的位置,在重写的onLayout方法里我们会将子View遍历出来然后再调用它们的layout方法布局它们的位置。

从上我们知道,setFrame方法是真正布局View位置的方法,那我们就看看setFrame方法的具体实现。

<span style="font-size:12px;">protected boolean setFrame(int left, int top, int right, int bottom) {        boolean changed = false;        if (DBG) {            Log.d("View", this + " View.setFrame(" + left + "," + top + ","                    + right + "," + bottom + ")");        }        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);            mPrivateFlags |= PFLAG_HAS_BOUNDS;            ...........        }        return changed;    }</span>
从上述代码我们可以看出,setFrame这个方法主要就是赋值View的左上右下值。

至此,layout过程基本就完成了。那layout完成之后我们又能得到什么呢?

我们能得到getWidth()和getHeight()的值,是不是很熟悉?跟前面measure完成之后能得到的getMeasuredWidth()和getMeasuredHeight()是不是很相似?但是千万别将他们混为一谈哦,虽然他们在一般情况下是相等的,但是他们的内部实现原理是完全不同的,不信请看源码

<span style="font-size:12px;">public final int getWidth() {        return mRight - mLeft;    }</span>

<span style="font-size:12px;">public final int getHeight() {        return mBottom - mTop;    }</span>

是不是非常简洁明了,右边减去左边就是宽,下边减去上边就是高。所以如果你要变态到要去手动更改宽高值的话,你也只需要重写View的layout方法改变一下传入的左上右下值得大小。


三、draw过程,该过程也是最神奇的一个过程,因为这个过程才是真正将内容展示在屏幕上让我们能够看到。

     由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不

  会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该

视图需要重绘时,就会为该View添加该标志位。如果是继承ViewGroup,则会遍历出它所有的子View并对其子View进行绘制。


     从源码我们可以看到,draw过程一共有六步,那我们就结合源码中的英文注释来看看是哪六步:

     

<span style="font-size:12px;">1. Draw the background</span>
     第一步画背景。background.draw(canvas)

<span style="font-size:12px;">2. If necessary, save the canvas' layers to prepare for fading</span>
     第二步,如果有需要的话,保存画布的图层为显示渐变做准备。

<span style="font-size:12px;">3. Draw view's content</span>
     第三步,绘制内容。(onDraw)

<span style="font-size:12px;">4. Draw children</span>
    第四步,绘制子View。(dispatchDraw)

<span style="font-size:12px;">5. If necessary, draw the fading edges and restore layers</span>
    第五步,如果有需求的话,画渐变框并恢复图层
<span style="font-size:12px;">6. Draw decorations (scrollbars for instance)</span>
   第六步,画装饰(onDrawScrollBars)。


对于draw过程,可以说是最简单也可以说是最复杂的,简单的在于Google已经帮我们吧draw框架写好了,所以我们在自定义ViewGroup的时候不用管draw过程,只需要实现measure和layout过程就可。复杂的在于,我们写继承View的自定义控件的时候是一定要重写onDraw方法的,这样才能绘制出你自定义的View的内容。而onDraw(Canvas canvas)方法中最重要的两个东西则是Paint(画笔)和Canvas(画布)。这两个类给自定义View带来了千奇百怪的变化,想要要写出好的自定义View那我们就应该去熟悉Paint和Canvas中的诸多API方法,然后通过不断的练习才能做到随心所欲绘制出各种奇形怪状的自定义控件。我这里给大家推荐自定义控件大神爱哥的系列博客"自定义控件其实很简单",笔者这篇文章就当成抛砖引玉,希望大家能有所收获。





     







5 0