对于稍有自定义View经验的安卓开发者来说,onMeasure,onLayout,onDraw这三个方法都不会陌生,起码多少都有所接触吧。
在安卓中,一个View显示到屏幕上基本上都是经过测量,摆放,绘制这三个过程才显示出来,那么这三个过程到底是怎么执行的呢?本文与大家一起探讨一下安卓中View的绘制流程。
一,View树绘制流程开始的地方(API23)
对一个布局进项测量,摆放,绘制肯定要有开始的地方吧,这里就直接跟大家说了,View绘制流程开始的地方是ViewRootImpl类的performTraversals()方法(至于为什么是这里不是本篇重点,后续有时间写一篇针对这里的文章说明一下),接下来我们看下performTraversals()方法(此方法过长,只列出重要逻辑代码)
- private void performTraversals() {
- ......
- int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
- int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
- ......
- // Ask host how big it wants to be
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- ......
- performLayout(lp, desiredWindowWidth, desiredWindowHeight);
- ......
- performDraw();
- ......
- }
第3,4行代码调用getRootMeasureSpec方法生成对应宽高,我们先看下getRootMeasureSpec都做了什么,源码如下:
- /**
- * Figures out the measure spec for the root view in a window based on it's
- * layout params.
- *
- * @param windowSize
- * The available width or height of the window
- *
- * @param rootDimension
- * The layout params for one dimension (width or height) of the
- * window.
- *
- * @return The measure spec to use to measure the root view.
- */
- 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这个概念:也叫测量规格,MeasureSpec是一个32位整数,由SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,低30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。
View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams结合父View的MeasureSpec生成(普通View的MeasureSpec是由其父类ViewGroup生成的,后面会详细讲到)。
SpecMode的取值可为以下三种:
- MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
- MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
- MeasureSpec.UNSPECIFIED //未指定模式,父View对子View的大小不做限制,完全由子View自己决定;
- getRootMeasureSpec方法就是生成根视图的MeasureSpec,还记得我们上一篇《Android之View绘制流程开胃菜---setContentView(...)详细分析
- 》中分析的吗,平时我们自己写的布局都是被添加到DecorView中id为content的布局中的,
- 这里传入进来的windowSize参数是window的可用宽高信息,rootDimension宽高参数均为MATCH_PARENT。
我们上面说普通View的MeasureSpec是由其父类ViewGroup生成的,但是根视图
- DecorView是没有父类的,所以getRootMeasureSpec就是给根视图生成测量规格的,生成的MeasureSpec中SpecMode为MeasureSpec.EXACTLY,SpecSize则为窗口的可用尺寸。
回到performTraversals()方法中:
3,4行代码分别生成宽高的测量规格
7,9,11行代码分别执行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp, desiredWindowWidth, desiredWindowHeight),performDraw();
方法,我们看下这三个方法源码:都经过简化处理
- 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);
- }
- }
- private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
- int desiredWindowHeight) {
- ...
- final View host = mView;
- host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
- ...
- }
- private void performDraw() {
- ...
- draw(fullRedrawNeeded);
- ...
- }
- private void draw(boolean fullRedrawNeeded) {
- ...
- if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
- return;
- }
- ...
- }
- private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
- boolean scalingRequired, Rect dirty) {
- ...
- mView.draw(canvas);
- ...
- return true;
- }
- performMeasure方法最核心的是第4行调用mView的measure方法。
- performLayout方法通过5,7行代码发现其实也是调用的mView的layout方法。
- performDraw最终调用的也是调用的mView的draw方法。
- 上面的mView就是DecorView,我们知道DecorView是FrameLayout,FrameLayout继承自ViewGroup,ViewGroup继承自View,
- 所以最终都会调用View类中measure,layout,draw方法。
- 实际上View的绘制流程可以分为三个阶段:
- 大体流程如图:
- 二,View绘制流程第一步measure过程分析(API23)
- 接下来我们看下View中的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) {...
- // measure ourselves, this should set the measured dimension flag back
- onMeasure(widthMeasureSpec, heightMeasureSpec);...
- }
注释已经给出大体描述:这个被调用用来测算出view大小,并且其父类提供了约束信息widthMeasureSpec与heightMeasureSpec。
我们发现measure方法被final修饰,所以这个方法不能被子类重写。
实际的测量是在onMeasure方法进行,所以在View的普通子类中需要重写onMeasure方法来实现自己的测量逻辑。
对于普通View,调用View类的onMeasure()方法来进行实际的测量工作即可,当然我们也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局。
接下来我们看下默认情况下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以及自身内容来决定宽高,子类应该重写这个方法提供更精确更高效的测量的内容。当重写这个方法的时候子类必须调用setMeasuredDimension(int, int)来存储已经测量出来的宽高。
我们看到系统默认的onMeasure方法只是直接调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。
接下来我们看看设置的默认View宽高,默认宽高都是通过getDefaultSize方法来获取的,而getDefaultSize又调用了getSuggestedMinimumXXX方法,我们先看下getSuggestedMinimumXXX方法:
- protected int getSuggestedMinimumHeight() {
- return (mBackground == null) ? mMinHeight: max(mMinHeight, mBackground.getMinimumHeight());
- }
- protected int getSuggestedMinimumWidth() {
- return (mBackground == null) ? mMinWidth: max(mMinWidth, mBackground.getMinimumWidth());
- }
- mMinHeight或mMinWidth就是我们设置的android:minHeight或android:minWidth参数。
- 如果我们没有设置背景则直接返回mMinHeight或mMinWidth,如果设置了背景,则返回miniXXX属性与mBackground二者中较大者。如背景以及miniXXX属性都没设置呢?那就返回0了。
- 接下来再看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;
- }
- getDefaultSize返回值由上面讲到的getSuggestedMinimumXXX方法获取的Size以及父类传递过来的measureSpec共同决定。
- 可以看到如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的规格。
- 到此为止,普通View(非ViewGroup)的测量就基本讲完了。但是ViewGroup这种容器类布局是怎么测量其内每个子View的呢?
- ViewGroup容器类布局大部分情况下是用来嵌套具体子View的,所以需要负责其子View的测量,在ViewGroup中定义了
- measureChildren(int widthMeasureSpec, int heightMeasureSpec)
- measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec)
- 以及measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)
- 三个方法来供其子类调用对具体子View进行测量。
- measureChildren
- ,
- measureChild
- 源码如下:
- 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);
- }
- }
- }
- 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);
- }
看到了吧,measureChildren只是循环调用measureChild方法,而measureChild方法中会根据父类提供的测量规格parentXXXMeasureSpec一级子类自己LayoutParams调用getChildMeasureSpec方法生成子类自己具体的测量规格。(getChildMeasureSpec稍后会具体分析)
接下来我们看下measureChildWithMargins方法源码:
- 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相比最主要的区别就是measureChildWithMargins额外将具体子View LayoutParams参数的margin也当作参数来生成测量规格。
measureChild与measureChildWithMargins均调用了getChildMeasureSpec方法来生成具体测量规格,接下来我们重点看下这个方法:
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
- int specMode = MeasureSpec.getMode(spec); //获取父View的mode 3 int specSize = MeasureSpec.getSize(spec);//获取父View的size
- //父View的size减去padding与0比较取其大,specSize - padding得到的值是父View可以用来盛放子View的空间大小
- 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:
- //父View希望子View是明确大小
- if (childDimension >= 0) { //子View设置了明确的大小:如 10dp,20dp
- resultSize = childDimension; //设置子View测量规格大小为其本身设置的大小
- resultMode = MeasureSpec.EXACTLY; //mode设置为EXACTLY
- } else if (childDimension == LayoutParams.MATCH_PARENT) { //子VIEW的宽或者高设置为MATCH_PARENT,表明子View想和父View一样大小
- // Child wants to be our size. So be it.
- resultSize = size; //设置子View测量规格大小为父View可用空间的大小
- resultMode = MeasureSpec.EXACTLY; //mode设置为EXACTLY
- } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子VIEW的宽或者高设置为WRAP_CONTENT,表明子View大小是动态的
- // Child wants to determine its own size. It can't be
- // bigger than us.
- resultSize = size; //设置子View测量规格大小为父View可用空间的大小
- resultMode = MeasureSpec.AT_MOST; //mode设置为AT_MOST,表明子View宽高最大值不能超过resultSize
- }
- break;
- 27 //其余情况请自行分析
- ......
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
想说的注释已经给出。
上面的方法展现了根据父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的过程, 子View的LayoutParams表示了子View的期待大小。这个产生的MeasureSpec用于指导子View自身的测量。
在我们自定义View的时候一般会重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法其中的widthMeasureSpec与heightMeasureSpec参数就是父类通过getChildMeasureSpec方法生成的。一个好的自定义View会根据父类传递过来的测量规格动态设置大小,而不是直接写死其大小。
好了,到此为止View的测量过程想说的就差不多都说完了,我们稍微总结一下关键的部分;
- 最后附上View绘制流程图:相信你会理解的更加深刻:
- 三,View绘制流程第二步layout过程分析 (API23)
- performMeasure执行完,接着就会执行performLayout:
- private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
- int desiredWindowHeight) {
- ...
- final View host = mView;
- host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
- ...
- }
- mView为根View,即DecorView,
- DecorView是FrameLayout的子类,最终会调用ViewGroup中layout方法。所以接下来我们看下ViewGroup中layout方法源码:
- @Override public final void layout(int l, int t, int r, int b) {
- if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
- if (mTransition != null) {
- mTransition.layoutChange(this);
- }
- super.layout(l, t, r, b);
- } else {
- // record the fact that we noop'd it; request layout when transition finishes
- mLayoutCalledWhileSuppressed = true;
- }
- }
第7行代码表明又调用父类View的layout方法。所以我们看下View的layout源码,如下:
- 1 public void layout(int l, int t, int r, int b) {
- 2// l为本View左边缘与父View左边缘的距离
- // t为本View上边缘与父View上边缘的距离
- // r为本View右边缘与父View左边缘的距离
- // b为本View下边缘与父View上边缘的距离
- ...
- 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);
- ...
- }
4,5行代码主要判断View的位置是否发生变化,发生变化则changed 会为true,并且setOpticalFrame也是调用的setFrame方法
我们看下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;
- ...
- mLeft = left;
- mTop = top;
- mRight = right;
- mBottom = bottom;
- ...
- }
- return changed;
- }
- 第6行代码分别比较之前的记录的mLeft,mRight,mTop,mBottom 与新传入的参数如果有一个不同则进入判断,将changed变量置为true,并且将新传入的参数分别重新赋值给mLeft,mRight,mTop,mBottom,最后返回changed。
这里还有一点要说,getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别,先看一下源码;
- public final int getMeasuredWidth() {
- return mMeasuredWidth & MEASURED_SIZE_MASK;
- }
- public final int getMeasuredHeight() {
- return mMeasuredHeight & MEASURED_SIZE_MASK;
- }
- public final int getWidth() {
- return mRight - mLeft;
- }
- public final int getHeight() {
- return mBottom - mTop;
- }
在讨论View的measure过程时提到过mMeasuredWidth与mMeasuredHeight只有测量过程完成才会被赋值,所以只有测量过程完成调用getMeasuredWidth()、getMeasuredHeight()才会获取正确的值。
同样getWidth()、getHeight()只有在layout过程完成时
调用才会获取正确返回值,所以二者调用时机是不同的。
- mLeft,mRight,mTop,mBottom才会被赋值,
继续看View中layout源码第7行,如果changed为true,也就是说View的位置发生了变化,或者标记为PFLAG_LAYOUT_REQUIRED则进入判断执行onLayout方法。
我们继续看View中onLayout方法源码:
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- }
看到了吧,竟然是个空方法。
对比View的layout和ViewGroup的layout方法发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,那么容器类View是怎么对其子View进行摆放的呢?别急,在ViewGroup中同样也有onLayout方法,源码如下;
- /**
- * {@inheritDoc}
- */
- @Override
- protected abstract void onLayout(boolean changed,
- int l, int t, int r, int b);
- 看到了吧,还是个抽象方法,因为具体ViewGroup摆放规则不同,所以其具体子类需要重写这个方法来实现对其子View的摆放逻辑。
既然这样我们就只能分析一个继承自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();
- ......
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() != GONE) {.....
- child.layout(childLeft, childTop, childLeft + width, childTop + height);
- }
- }
- }
看到了吧,onLayout方法调用layoutChildren方法,在layoutChildren方法中遍历每个子View调用其layout方法。
好了,到此Layout过程就讨论的差不多了,相比measure过程还是简单不少的,其也是递归调用逻辑。如图:
我们总结一下主要部分:
测量,摆放过程都分析完了,接下来我们分析View的draw过程。
四,View绘制流程第三步draw过程分析(API23)
performMeasure, performLayout过程执行完,接下来就执行performDraw()逻辑了,ViewGroup没有重写View的draw方法,最终调用的是View中的draw方法,源码如下:
- public void draw(Canvas canvas) {
- al int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && ttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
- /*
- * 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;
- }...
- // Step 2, save the canvas' layers
- ....
- // Step 3, draw the content
- if (!dirtyOpaque) onDraw(canvas);
- // Step 4, draw the children
- dispatchDraw(canvas);
- // Step 5, draw the fade effect and restore layers
- ....
- // Step 6, draw decorations (foreground, scrollbars)
- onDrawForeground(canvas);
- }
5到14行注释可以看到draw过程分为6步,再看23行提示大部分情况下跳过第2,5步。所以我们着重分析其余4步。
19-21行执行第一步,绘制背景源码如下:
- private void drawBackground(Canvas canvas) {
- final Drawable background = mBackground;
- if (background == null) {
- return;
- }
- ....
- setBackgroundBounds();
- ....
- background.draw(canvas);
- }
- void setBackgroundBounds() {
- if (mBackgroundSizeChanged && mBackground != null) {
- mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
- mBackgroundSizeChanged = false;
- rebuildOutline();
- }
- }
只要逻辑就是获取我们在xml文件或者代码中设置的背景,然后根据layout过程摆放的位置绘制出来。
第29行执行绘制内容逻辑,源码如下:
- /**
- * Implement this to do your drawing.
- *
- * @param canvas the canvas on which the background will be drawn
- */
- protected void onDraw(Canvas canvas) {
- }
看到了吧,是一个空方法,需要具体子类自己去实现,因为每个具体View要绘制的内容是不同的,所以子类需要实现这个方法来绘制自身的内容。
第32行执行绘制子View逻辑,源码如下:
- /**
- * Called by draw to draw the child views. This may be overridden
- * by derived classes to gain control just before its children are drawn
- * (but after its own view has been drawn).
- * @param canvas the canvas on which to draw the view
- */
- protected void dispatchDraw(Canvas canvas) {
- }
- 看到了吧,也是空方法,这个方法被用来绘制子View的,如果有子View则需要调用这个方法去绘制,我们知道一般只有容器类View才可以盛放子View,所以我们看下ViewGroup中有没有相关逻辑,在
- ViewGroup中果然实现了这个方法,源码如下:
- @Override
- protected void dispatchDraw(Canvas canvas) {
- boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
- final int childrenCount = mChildrenCount;
- final View[] children = mChildren;
- .......
- 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);
- }
- .......
- }
- ......
- }
- ......
- }
- 在dispatchDraw方法中遍历每个子View并且调用drawChild方法,接下来我们看下drawChild源码:
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- return child.draw(canvas, this, drawingTime);
- }
- 看到了吧,最终调用每个子View的draw方法来完成自身的绘制。
接下来40行执行onDrawForeground逻辑,这一部分只要绘制一些装饰物,比如ScrollBar。这部分就不分析了,也不是重点。
到这里,View的主要绘制流程我们也分析完了,也不复杂。
但是,但是!!!!!!细心的你有没有发现在执行第一步,第三步的时候都有个if判断(if (!dirtyOpaque)),也就是说只有判断成立才会执行绘制背景和自身内容,难道还有View不绘制自身内容吗? 这里就直接说了,ViewGroup子类默认情况下就是不执行onDraw方法的,在ViewGroup源码中的initViewGroup()方法中设置了一个标记,源码如下:
- private void initViewGroup() {
- // ViewGroup doesn't draw by default
- if (!debugDraw()) {
- setFlags(WILL_NOT_DRAW, DRAW_MASK);
- }
- ......
- }
看第二行注释也知道,ViewGroup默认情况下是不会draw的。
第四行调用setFlags方法设置标记WILL_NOT_DRAW,
我们在回到View中draw方法看第2行代码:
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
setFlags方法就是对View中mPrivateFlags值进行相应改变,我们设置标记WILL_NOT_DRAW那么dirtyOpaque得到的值就为true,从而if (!dirtyOpaque)不成立,也就不会执行onDraw方法。
估计这点很多同学有疑问ViewGroup默认情况下onDraw方法是不执行的???别急,动手写个小demo验证一下就是了。
布局如下:极其简单
- <com.wanglei.clearheart.MyView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center"
- android:orientation="vertical"
- tools:context=".MainActivity" >
- </com.wanglei.clearheart.MyView >
MyView源码如下:同样极其简单
- public class MyView extends ViewGroup {
- private Paint mPaint;
- public MyView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mPaint = new Paint();
- mPaint.setColor(Color.RED);
- mPaint.setStyle(Style.STROKE);
- mPaint.setStrokeWidth(10);
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- // TODO Auto-generated method stub
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // TODO Auto-generated method stub
- canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, 360, mPaint);
- }
- }
运行程序会看到就是一个大白板,没有绘制出任何图形,那我们怎么让ViewGroup调用onDraw方法呢?
很简单View类中给我们提供了一个方法供外部调用:
- public void setWillNotDraw(boolean willNotDraw) {
- setFlags(willNotDraw ? WILL_NOT_DRAW: 0, DRAW_MASK);
- }
看到了吧,本质也是调用的setFlags方法。如果我们传入true则绘制的时候不会调用onDraw方法,传入false则使其调用onDraw方法。
我们修改MyView代码:构造方法中调用setWillNotDraw(false);
- public MyView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mPaint = new Paint();
- mPaint.setColor(Color.RED);
- mPaint.setStyle(Style.STROKE);
- mPaint.setStrokeWidth(10);
- setWillNotDraw(false);
- }
运行程序,会看到手机屏幕中间画出一个红色的圆。还有一种方法我们在布局中给MyView添加背景同样会达到调用onDraw方法的目的。
容器类布局(ViewGroup子类)为什么默认情况下不绘制背景和自身内容呢?答案是为了性能啊,大家想想容器类布局如果没有背景,只是用来盛放子类有必要调用onDraw方法吗?
有什么可绘制的吗?子类会自己实现onDraw方法绘制自己内容的。
接下来我们总结一下draw流程的重点:
最后附上draw流程图:
好了,到此本篇就该结束了,用了很长的篇幅来探讨View的绘制流程,希望对大家有用,废话就不多说了,咱们下篇见。
来源: http://www.cnblogs.com/leipDao/p/7573803.html