Android源码阅读系列(五)之View绘制流程

一、引言

通过Android官方文档可以知道,View的绘制有三个主要的流程,也就是measure、layout和draw。那View绘制为什么要走这三个流程呢?源码中有答案。

二、measure源码分析

先看一下measure的源码

/**
 * <p>
 * This is called to find out how big a view should be. The parent
 * supplies constraint information in the width and height parameters.
 * </p>
 *
 * <p>
 * The actual measurement work of a view is performed in
 * {@link #onMeasure(int, int)}, called by this method. Therefore, only
 * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
 * </p>
 *
 *
 * @param widthMeasureSpec Horizontal space requirements as imposed by the
 *        parent
 * @param heightMeasureSpec Vertical space requirements as imposed by the
 *        parent
 *
 * @see #onMeasure(int, int)
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......

     onMeasure(widthMeasureSpec, heightMeasureSpec);

    ......
}

从方法说明可以得知,这个方法是用于计算View的大小,View的宽高是由父视图决定的。实际的测量工作是在onMeasure方法进行,由于measure是final,所以在View的子类只能重写onMeasure方法。从源码可以看出,measure方法调用了View的onMeasure方法,看下View的onMeasure源码

/**
 * <p>
 * Measure the view and its content to determine the measured width and the
 * measured height. This method is invoked by {@link #measure(int, int)} and
 * should be overridden by subclasses to provide accurate and efficient
 * measurement of their contents.
 * </p>
 *
 * <p>
 * <strong>CONTRACT:</strong> When overriding this method, you
 * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
 * measured width and height of this view. Failure to do so will trigger an
 * <code>IllegalStateException</code>, thrown by
 * {@link #measure(int, int)}. Calling the superclass'
 * {@link #onMeasure(int, int)} is a valid use.
 * </p>
 *
 * <p>
 * The base class implementation of measure defaults to the background size,
 * unless a larger size is allowed by the MeasureSpec. Subclasses should
 * override {@link #onMeasure(int, int)} to provide better measurements of
 * their content.
 * </p>
 *
 * <p>
 * If this method is overridden, it is the subclass's responsibility to make
 * sure the measured height and width are at least the view's minimum height
 * and width ({@link #getSuggestedMinimumHeight()} and
 * {@link #getSuggestedMinimumWidth()}).
 * </p>
 *
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 *
 * @see #getMeasuredWidth()
 * @see #getMeasuredHeight()
 * @see #setMeasuredDimension(int, int)
 * @see #getSuggestedMinimumHeight()
 * @see #getSuggestedMinimumWidth()
 * @see android.view.View.MeasureSpec#getMode(int)
 * @see android.view.View.MeasureSpec#getSize(int)
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

方法说明里面也强调了这个方法是需要在子类重写的,看方法源码里面就调用了一个函数,设置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.
 *
 * @param size Default size for this view
 * @param measureSpec Constraints imposed by the parent
 * @return The size this view should be.
 */
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

可以看出返回值分两种情况,先看第一种MeasureSpec.UNSPECIFIED,在onMeasure我们可以得知,size这个值是通过getSuggestedMinimumWidth/getSuggestedMinimumHeight得到的,看下它们的源码

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

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

以getSuggestedMinimumWidth为例,如果View没有设置背景,那么View的宽度即为mMinWidth,也即android:minWidth这个参数的值;如果View设置了背景,需要看下mBackground.getMinimumWidth的源码

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

/**
 * Returns the drawable's intrinsic width.
 * <p>
 * Intrinsic width is the width at which the drawable would like to be laid
 * out, including any inherent padding. If the drawable has no intrinsic
 * width, such as a solid color, this method returns -1.
 *
 * @return the intrinsic width, or -1 if no intrinsic width
 */
public int getIntrinsicWidth() {
    return -1;
}

可以看出getMinimumWidth返回了Drawable的原始宽度,如果没有原始宽度,返回了0。通过上面的分析可以知道,MeasureSpec.UNSPECIFIED这种情况是由系统测量的。所以我们应该关注MeasureSpec.AT_MOST和MeasureSpec.EXACTLY这两种情况,也就是说如果是自定义View,那View的宽高是由specSize决定的。

上面提到三种MeasureSpec,可以分别看下他们的实际含义

MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定; 

三、layout源码分析

看了measure过程,知道了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);
        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;
}

从源码可以得知,layout首先确定了View四个顶点的位置,顶点位置确定,其在父视图中的位置也就确定了。接着类似measure过程,又调用了onLayout方法

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

可以看到,View里面这是一个空方法,我们需要到具体实现里面看了,这里可以看下LinearLayout

@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里面的实现和onMeasure实现过程类似。

三、draw源码分析

通过measure和layout,View的大小和位置已经确定了,接下来就要绘制View到屏幕了。

public void draw(Canvas canvas) {
    ......
    /*
     * 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)
     */

    // 第一步,对View的背景进行绘制
    ......
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    ......

    // 第二步,绘制内容
    if (!dirtyOpaque) onDraw(canvas);

    // 第三步,绘制子View
    dispatchDraw(canvas);

    // 第四步,绘制滚动条
    onDrawScrollBars(canvas);
    ......
}

可以看到,通过上面4个步骤,将View绘制到了屏幕上。