View的绘制流程


image.png

导言

最近在研究View的绘制流程,网上很多相关博客,从源码角度分析整个绘制流程,其实主要就是三个步骤:测量、布局、绘制,网上大神们讲解的很深入,还有的针对measure过程给出示例,清晰而形象地分析了测量的整个流程。
不过,不过,虽然看了这么多,但只有以自己的理解写出来,才能真正的转换为自己的东西。
下面,我会是基于Andriod(Api26)源码分析,按先广后深的思路引导大家理解View的整个绘制流程,即先了解整体流程,再一点点深入到细节中去,希望能从另外一个角度帮助大家加深理解。

Activity视图结构

首先,请看下面一张图


view_Activity视图结构.png

从上图很容易看出Activity整体视图结构,接着,我们来分析其中的一些重点:

  • PhoneWindow继承Window类,Window是个抽象类,是窗口的概念。有人会问,窗口是什么以及有什么作用?在Android中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface通过WindowManageService分配。Surface可以理解为一块画布,应用可以通过Canvas在其上作画。画好之后SurfaceFlinger将多块Surface以特定的顺序输入到FrameBuffer中,这样界面就得以显示。
  • 每个Activity都会创建一个PhoneWindow对象,而每个Window对应一个View和一个ViewRootImpl,Window和View通过ViewRootImpl建立联系。
  • DecorView是视图的根View,本质是一个FrameLayout,它下面包含一个vertical方向的LinearLayout,LinearLayout下面又包括一个TitleView和一个ContentView,TitleView可以通过requestWindowFeature(Window.FEATURE_NO_TITLE)去掉,而setContentView(R.layout.activity_main)就是往我们的ContentView中设置内容。

Activity视图初始化入口

下面,开始分析一下Activity视图初始化流程。首先,还是先看一张图


view_Activity视图创建流程.png

接着,我们就从最熟悉的Activity.setContentView(R.layout.activity_main)来分析:

  1. 在Activity的onCreate方法中,我们会调用setContentView方法:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);
        ...
    }
  1. 跟进setContentView方法看
    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
  1. 继续往下跟会发现最后调用的是PhoneWindow的setContentView方法
@Override
public void setContentView(int layoutResID) {
    // mContentParent是上面提到的ContentView的父容器,如为空,则调用installDecor()初始化decorView和mContentParent
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ...
    } else {
     //一般情况下回来到这里,把我们传入的布局添加到mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
}
  1. 继续跟进LayoutInflater.inflate方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

//此方法中省略很多代码,只摘出相关的代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    View result = root;
    if (TAG_MERGE.equals(name)) {
        rInflate(parser, root, inflaterContext, attrs, false);
    } else {
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        rInflateChildren(parser, temp, attrs, true);
        result = temp;
    }
    return result;
}
  1. 从上面可以看出,merge标签做了单独处理,如果是merge标签,则调用rInflate方法,如果是非merge,则调用rInflateChildren方法,分别看两个方法
void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
  1. 可以看出来,rInflate方法实际是遍历解析每个标签,并且把它加载父View中,在此过程中,如果是ViewGroup,就会调用rInflateChildren方法
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
  1. rInflateChildren实际还是调用rInflate方法,通过递归调用,最终把布局中的ViewTree添加到mContentView中。至此,整个setContentView的流程分析完毕。

视图绘制流程

接下来,开始分析ViewRootImpl是如何完成View绘制的。当decorView与ViewRootImpl关联好之后,会调用ViewRootImpl的requestLayout方法

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

方法中调用的scheduleTraversals()方法调度一次完整的绘制流程

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

该方法会发送一个消息,去执行遍历任务mTraversalRunnable

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

可以看到,在doTraversla()方法中会调用performTraversals()方法,重点来了,performTraversals()方法就是整个View绘制流程的起点。

//此方法内容太多,只摘取绘制相关的主要代码
private void performTraverslas() {
    ......
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
     ......
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    performLayout(lp, mWidth, mHeight);
    ......
    performDraw();
    ......
}

//获取窗口的测试测量参数
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;
}

performMeasure()方法中的关键代码

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

performLayout()方法中的关键代码

mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

performDraw()方法通过层层调用,最后会走到

mView.draw(canvas);

通过上面代码分析可看到,View的绘制流程分为三个步骤,分别是measure,layout,draw。而代码中的mView即是DecorView,下面来看一下三个步骤分别干了什么事情。

1、测量measure

了解MeasureSpec

  • MeasureSpec(测量参数或测量规格):它封装的是父容器传递给子容器的测量要求,简单一点理解就是,通过父容器的MeasureSpec和子容器的LayoutParam计算出子容器的测量要求,这个测量要求就是MeasureSpec。
  • MeasureSpec是一个int类型的组合值,由前2位的mode(测量模式)和后30位的size(尺寸)组合而成,是为了节约对象分配开支。

测量模式有三种:

  • EXACTLY : 父容器大小已确定,子容器最大不能超过父容器
  • AT_MOST : 父容器大小未定,但不能超过声明大小,因此子容器也不能超过声明大小
  • UNSPECIFIED : 父容器对子容器没有任何限制,子容器想要多大就多大

子容器的LayoutParam也有三种:

  • match_parent : 适应父容器的尺寸
  • wrap_content : 适应自身内容的尺寸
  • dimension(eg. 100dp) : 固定尺寸

根据父容器的MeasureSpec和子容器的LayoutParam计算子容器的MeasureSpec:

view_计算子容器MeasureSpec.jpg

View的measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

主要调用了onMeasure方法,那么onMeasure方法又干了什么呢

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

主要就是设置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,如果测量模式为EXACTLY或AT_MOST,就取测量尺寸作为自己的尺寸,如果是UNSPECIFIED,则使用默认尺寸。那么默认尺寸是什么呢

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

}

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

可见默认尺寸是我们在布局文件中设置的minWidth、minHeight或者是背景图片的大小。大多数情况会直接使用测量的值作为实际的宽高。如果想改变View的测量大小,可以直接通过setMeasureDimension设置(不建议)。但是在onMeasure方法中必须调用setMeasureDimension方法,不然会抛异常。

ViewGroup的onMeasure方法
看了View的测量方法之后,我们来看一下ViewGroup的测量方法。打开ViewGroup的源码一搜,咦,咦,咦,ViewGroup竟然没有实现onMeasure方法。那么仔细想想,具体的实现肯定是放在子类中。我们就以FrameLayout为例,来分析一下吧。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    ...
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //遍历子View,只要不是GONE,都会测量,并且记录下最大的width和最大的height
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    //所有子View测量完毕之后,通过setMeasureDimension设置自己的宽高
    //对于FrameLayout,是使用最大的View的大小,而对于LinearLayout,可能是高度的累加
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
}

在该方法中,会调用measureChildWidthWithMargins()方法,实现如下:

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

该方法先计算出子View的测量规则,再调用子View的measure方法来测量子View。如果子View是View类型的,会按照View的measure方法进行测量,如果子View是ViewGroup,则继续按照ViewGroup的onMeasure方法进行测量。
该方法会调用getChildMeasureSpec()计算子View的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);
}

整个的计算逻辑很简单,就如上图计算子容器MeasureSpec中所总结。

一个简单的测量例子

至此,View的测量过程已经讲完,包括View的测量方法和ViewGroup的测量方法。下面,我们通过一个简单的布局来介绍测量的具体流程,视图布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/llRoot"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvContent1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="content1" />

    <TextView
        android:id="@+id/tvContent2"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:text="content2" />
</LinearLayout>

布局很简单,就是一个vertical的LinearLayout中包含两个TextView,为了方便起见,设置窗口为FEATURE_NO_TITLE,从下面时序图很容易看出测量流程:


view_简单布局测量流程.png

测量从DecorView开始,一层一层往下测量,棕色的方块表示传递height对应的MeasureSpec

2、布局layout

测量完毕之后,知道了每个View的大小,接下来就需要把View放到对应的地方,也就需要调用layout方法进行布局。

View的layout方法

public void layout(int l, int t, int r, int b) {
    ...
    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);
        ...
    }
    ...

如果View的位置改变,则需要对View进行重新布局,调用onLayout方法:

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

可见,View的onLayout方法是空实现,因为View没有子View,所以不需要进行布局,布局仅对ViewGroup有效,那么我们就来分析一下ViewGroup的onLayout方法。

ViewGroup的onLayout方法

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

ViewGroup的onLayout方法是抽象方法,可见具体的实现还是在ViewGroup的各种继承类中,在此还是来分析FrameLayout的onLayout方法:

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

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

这段代码中出现了l,t,r,b和parentLeft,parentRight,parentTop,parentBottom。我们通过下面一张图来理解这些值分别是什么意思


view_布局中的几个值.png

如图可知l,t,r,b表示当前ViewGroup在父容器中的边距,而parentLeft,parentRight,parentTop,parentBottom则是当前ViewGroup计算出自己的内边距,提供给子View进行布局。
理解了这几个值的意思之后,我们来分析代码中进行了什么操作,主要就是一个for循环,遍历子View并且确定每个子View在父View中的位置,在确定子View位置的过程中,会使用子View的LayoutParams,layoutDirection等一系列参数。

3、绘制draw

介绍完了测量和布局,代表View的大小和位置都已经确定了,接下来就需要把View绘制出来。

View的draw方法

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

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

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

源码中的注释很详细,绘制总共分6个步骤,其中第二步和第五步基本可以跳过,剩下的步骤是:

  • 绘制背景
  • 通过onDraw()绘制之身
  • 通过dispatchDraw()绘制子View
  • 绘制滚动条

接下来看看View的onDraw方法

/**
 * Implement this to do your drawing.
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {
}

空实现,也就是具体的绘制过程放在每个子View中了,由于View没有子View所以dispatchDraw自然也是空实现。

ViewGroup的draw

ViewGroup并没有覆写draw()和onDraw()方法,但会重写dispatchDraw()方法,此处就不给出dispatchDraw方法的源码,主要流程也是遍历子View,然后调用View的draw()方法。

至此View的绘制流程都已介绍完毕。下面我们来说一下绘制过程中的一些问题。

几个问题

1、measure和layout过程会执行多次

有些控件在测量的过程中确实存在多次测量的情况,比如RelativeLayout,LinearLayout中使用layout_weight。因为通过一次测量不能完全测出所有子View的宽高,所以需要多次测量。具体每个控件的测量过程就不在这里详细说明,各位可以自己 查看源码。

2、onMeasure、onLayout、onDraw在自定义view中的作用

  • 自定义View:如果控件直接继承自View,那么在布局中使用wrap_content的时候,控件会占据父容器的所有剩余空间,这种情况一般需要自己实现onMeasure方法。自定义View不需要重写onLayout,onDraw必须得自己实现。
  • 自定义ViewGroup:如果控件直接继承自ViewGroup,则必须重写onMeasure方法,也需要重写onLayout方法。
  • 继承自View或ViewGroup的子控件:在需要的时候去重写对应的方法。
    之后会有一篇文章专门介绍自定义View,在此就不做过多介绍。

总结

写了这么多,总算把View的绘制流程介绍完了,希望认真看完的小伙伴有所收获。本文主要从Activity的setContentView入手,介绍Activity 的视图初始化过程,当Activity进入resume时就开始界面的绘制流程。

写于2018.05.29 15:00(位置:深圳南山)

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

推荐阅读更多精彩内容