上一篇博客介绍了Android中Activity的页面组成,在其中说到了DecorView是View的根节点,也说到了PhoneWindow可以管理DecorView显示视图。但是我们将所有的View都增加到根节点之后,系统是怎样将这些View展示到用户眼前的呢?这篇博客就来介绍一下Android中View的绘制流程。

Android中View的绘制是从根节点开始的,这是一个自上而下的过程,主要需要进过三个步骤:Measure(测量大小) --> Layout(摆放位置) --> Draw(绘制内容)。我们知道了PhoneWindow类可以管理DecorView显示视图,其实是在PhoneWindow类中包含了ViewRootImpl对象,ViewRootImpl类里面就有一个方法performTraversals(),而View的展示过程就是从这个方法开始的。

PhoneWindow类中创建ViewRootImpl的方式如下:

private ViewRootImpl getViewRootImpl() {if (mDecor != null) {// 通过DecorView对象获取ViewRootImpl对象ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();if (viewRootImpl != null) {return viewRootImpl;}}throw new IllegalStateException("view not added");}
在ViewRootImpl类中的performTraversals()方法里面有很多的内容,但是与View的绘制过程相关的最重要的就是下面这几行:
privatevoid performTraversals(){……// 方法测量组件的大小performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);……// 方法用于子组件的定位(放在窗口的什么地方)performLayout(lp,desiredWindowWidth,desiredWindowHeight);……// 绘制组件内容performDraw();……}
而在performMeasure()、performLayout()和performDraw()方法的调用过程可以用下面的图来表示:
Android自定义View之View的绘制流程_第1张图片
从图中可以看出系统的View类已经写好了measure()、layout()和draw()方法:
① 在系统View类中measure()方法用了final修饰,不能被重写(我觉得这应该是google不想让开发者更改measure()方法里面的逻辑而设计的,但是开发者有时又有需求需要自己测量,所以提供了onMeasure()方法可以重写);
代码
② 在系统View类中的layout()方法在调用onLayout()方法前调用了setFrame()方法,这个方法作用是判断View的位置是否发生改变,如果没有发生改变,就不调用onLayout()方法,主要是为了提高性能,在View类中onLayout()方法是空实现,这是因为view没有子类,而当在自定义的控件如果是直接继承ViewGroup时就必须重写onLayout()方法;
代码
③ 在系统View类中的draw()方法,开发者一般不会重写,因为当我们如果重写draw()时,就需要按照系统定义好的步骤一步一步的画,否则会显示不出来,相对来说比较麻烦。而如果我们实现onDraw()方法,我们只要关注我们画的内容即可(画出来的内容就是显示到界面的内容);
代码

④ 当开发者在自定义控件时一般只需重写onMeasure()、onLayout()和onDraw()方法就可以了。


1、测量 -- measure

ViewRootImpl中的performMeasure()方法,调用了mView的measure()方法,mView表示View的根DecorView,也就是调用了FrameLayout的measure()方法,FrameLayout继承ViewGroup,ViewGroup没有重写也不能重写measure()方法,所以最终调用的是View类中的measure()方法:

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

在View类中的measure()中系统帮我们做了很多的处理并且不想让开发者重写measure的逻辑,所以使用了final修饰符进行修饰,并且调用了onMeasure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {...if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag backonMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}...}
所以在Activity中View树的测量过程中,最终是从FrameLayout的onMeasure()方法开始的,FrameLayout的onMeasure()方法如下:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int count = getChildCount();......// 判断孩子控件的Visibility属性,如果为gone时,就跳过,因为gone属性不占用空间if (count > 1) {// 判断是否有孩子控件for (int i = 0; i < count; i++) {// 通过LayoutParams参数获取孩子控件的margin、padding值...// 调用孩子控件的measure()方法测量自身大小child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}}}

在FrameLayout的onMeasure()方法中,显示获取了孩子控件的个数,然后获取每一个孩子控件并判断visibility属性;最终调用孩子View的measure()方法测量自身大小。

当开发者有需要重新测量控件时,只需要重写onMeasure()方法即可,系统在View的measure()方法中会回调onMeasure()方法,使测量值生效。

下面是继承自View时重写的onMeasure()方法,我就只是简单的将宽和高都设置成500:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//super.onMeasure(widthMeasureSpec, heightMeasureSpec);setMeasuredDimension(500,500);}

下面是继承ViewGroup时重写的onMeasure()方法,将所有孩子控件的宽和高的和计算出来作为父控件的宽和高,最终调用setMeasuredDimension()方法设置值:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int widthMeasure = 0,heightMeasure = 0;// 直接调用系统方法测量每一个孩子控件的宽和高measureChildren(widthMeasureSpec,heightMeasureSpec);/** * 系统在调用measureChildren(widthMeasureSpec,heightMeasureSpec)的过程中, * 如果孩子控件依然是ViewGroup类型的,那么又会调用measureChildren()方法,否则会调用 * child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法测量每一个孩子控件 * 的宽和高,直到所有的孩子控件都测量完成。 * 这就可以说明measure的过程可以看成是一个递归的过程。 */// 获取孩子控件的个数int childCount = getChildCount();// 循环测量每一个孩子控件的宽和高,得到的和就作为控件的宽和高for (int i = 0; i < childCount; i++) {View view = getChildAt(i);// 获取每一个孩子的宽和高int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();// 把每一个孩子控件的宽和高加上widthMeasure += width;heightMeasure += height;}// 调用setMeasuredDimension()方法保存宽和高,表示measure的结束setMeasuredDimension(widthMeasure,heightMeasure);}
另外在measure的过程中还可能会用到MeasureSpec类(View的内部类):

public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK  = 0x3 << MODE_SHIFT;// 父控件不没有对子施加任何约束,子可以是任意大小(也就是未指定)public static final int UNSPECIFIED = 0 << MODE_SHIFT;// 父控件决定子的确切大小,表示width和height属性设置成match_parent或具体值public static final int EXACTLY     = 1 << MODE_SHIFT;// 子最大可以达到的指定大小,当设置为wrap_content时,模式为AT_MOSTpublic static final int AT_MOST     = 2 << MODE_SHIFT;/* * 通过模式和大小创建一个测量规范 * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */public static int makeMeasureSpec(int size, 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);}/* * 获取模式 */public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}/* * 获取大小 */public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}...}
MeasureSpecs使用了二进制去减少对象的分配,用最高的两位数来表示模式(Mode),剩下的30位表示大小(size)。

Mode有三种:UNSPECIFIED(未指定,没有约束,可以任意大小)、EXACTLY(精确,表示match_parent或者具体的大小值)、AT_MOST(最大值,表示wrap_content)

measure总结:

① View的measure方法被final修饰,子类不可以重写,但可以通过重写onMeasure方法来测量大小,当然也可以不重写onMeasure方法使用系统默认测量大小;

② 如果想要让自己设置的值生效,就必须调用setMeasuredDimension()方法设置宽和高;

③ 如果在Activity的onCreate()方法或onResume()方法里面直接调用getWidth()/getHeight()、getMeasureWidth()/getMeasureHeight()获取控件的大小得到的结果很可能是0,因为在onCreate()或onResume()的时候系统还没有调用measure方法(getMeasureWidth()和getMeasureHeight()的赋值在View的setMeasuredDimension()方法中,所以在调用完View的setMeasuredDimension()方法之后getMeasuredWidth()和getMeasuredHeight()就已经有值了。而getWidth()和getHeight()要在onLayout()方法完成之后才会被赋值),如果一定要在onCreate()方法或onResume()方法里面获取控件的大小,可以通过以下方法得到:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {int width = view.getWidth();int height = view.getHeight();view.getViewTreeObserver().removeOnGlobalLayoutListener(this);}});
④ 通过setMeasuredDimension()方法设置的值并不一定就是控件的最终大小,组件真正的大小最终是由setFrame()方法决定的,该方法一般情况下会参考measure出来的尺寸值;

⑤ 子视图View的大小是由父容器View和子视图View布局共同决定的;

⑥ 如果控件是ViewGroup的子类,那就必须测量每一个孩子控件的大小,可以调用系统的measureChildren()方法测量,也可以自己测量;

⑦ Android系统对控件的测量过程可以看做是一个递归的过程。

2、摆放 -- layout

ViewRootImpl中的performLayout()方法作用是确定孩子控件的位置,所以该方法只针对ViewGroup容器类,最终调用了View的layout()方法确定每一个孩子控件的位置:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,            int desiredWindowHeight) {...final View host = mView;...host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());...for (int i = 0; i < numValidRequests; ++i) {final View view = validLayoutRequesters.get(i);Log.w("View", "requestLayout() improperly called by " + view +" during layout: running second layout pass");view.requestLayout();}}

代码中的host是View树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满 mContentParent容器。我们重点来看一下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);mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;ListenerInfo li = mListenerInfo;if (li != null && li.mOnLayoutChangeListeners != null) {ArrayList listenersCopy =(ArrayList)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()中确定位置之前会判断是否需要重新测量控件的大小,如果需要,就会调用onMeasure()方法重新测量控件,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,这一步并不会绘制出来,只是将控件位子和大小值保存起来;接着调用onLayout()方法,在View中onLayout()方法是空实现。onLayout()方法的作用是当当前控件是容器控件时,那就必须重写onLayout()方法确定每一个孩子控件的位置,而当孩子控件还是ViewGroup的子类时,继续调用onLayout()方法,直到所有的孩子控件都有了确定的位置和大小,这个过程和measure一样,也可以看做是一个递归的过程。下面是FrameLayout的onLayout()方法源码:

@Overrideprotected 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);... // 判断控件的visible属性书否为gone,如果为gone就不占用位置... // 计算childLeft、childTop、childRight、childBottom,确定位置child.layout(childLeft, childTop, childLeft + width, childTop + height);}}}

layout总结:

①  View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onLayout()方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout()方法,因为该方法在ViewGroup类中是抽象的,ViewGroup的所有子类必须实现onLayout()方法(如果我们定义的容器控件是继承FrameLayout或其他已经继承了ViewGroup类的容器控件时,如果没有必要可以不用实现onLayout()方法,因为FrameLayout类中已经实现了);

② 如果view控件使用了gone属性时,在onLayout()方法遍历中就会跳过当前的View,因为gone属性表示不占用位置;

③ 当layout()方法执行完成之后,调用getHeight()/getWidth()方法就能够得到控件的宽和高的值了;

④ View的layout过程和measure过程类似,都可以看做是一个递归的过程;

⑤ 在Activity中,layout的过程是从DecorView控件开始的。

3、绘制 -- draw

ViewRootImpl中的performDraw()方法,通过代码的追踪也可以看到最后调用了mView的draw()方法,mView是DecorView,也就是FrameLayout,在FrameLayout和ViewGroup中都是没有draw()方法的,所以最终调用的是View中的draw()方法:

private void performDraw() {      ...      final boolean fullRedrawNeeded = mFullRedrawNeeded;      mFullRedrawNeeded = false;      mIsDrawing = true;      Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");      try {          draw(fullRedrawNeeded);      } finally {          mIsDrawing = false;          Trace.traceEnd(Trace.TRACE_TAG_VIEW);      }        ...  }    private void draw(boolean fullRedrawNeeded) {      ...      if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {          return;      }      ...  }    /**  * @return true if drawing was succesfull, false if an error occurred  */  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,          boolean scalingRequired, Rect dirty) {      Canvas canvas;      ...      try {          ...          int left = dirty.left;          int top = dirty.top;          int right = dirty.right;          int bottom = dirty.bottom;          canvas = mSurface.lockCanvas(dirty);          if (!canvas.isOpaque() || yoff != 0) {              canvas.drawColor(0, PorterDuff.Mode.CLEAR);          }          mView.draw(canvas);          ...      } finally {          surface.unlockCanvasAndPost(canvas);      }      ...      return true;  }

从代码中可以看出canvas对象是从surface中获取到的,surface是中提供了一套双缓存机制,这样就提高了绘图的效率。

既然DecorView最终调用的是View中的draw()方法,那就看一下View中draw()方法的源码:

public void draw(Canvas canvas) {final int privateFlags = mPrivateFlags;/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * *      1. Draw the background   绘制视图View的背景 *      2. If necessary, save the canvas' layers to prepare for fading 保存画布canvas的边框参数 *      3. Draw view's content 绘制视图View的内容(调用了onDraw()方法) *      4. Draw children 绘制当前视图View的子视图(调用dispatchDraw()方法) *      5. If necessary, draw the fading edges and restore layers 绘制边框的渐变效果并重置画布 *      6. Draw decorations (scrollbars for instance) 绘制前景、滚动条等修饰 */// Step 1, draw the background, if needed 绘制视图View的背景int saveCount;if (!dirtyOpaque) {drawBackground(canvas);}// skip step 2 & 5 if possible (common case)...// Step 2, save the canvas' layers 保存画布canvas的边框参数saveCount = canvas.getSaveCount();// Step 3, draw the content 绘制视图View的内容(调用了onDraw()方法)if (!dirtyOpaque) onDraw(canvas);// Step 4, draw the children 绘制当前视图View的子视图(调用dispatchDraw()方法)dispatchDraw(canvas);// Step 5, draw the fade effect and restore layers 绘制边框的渐变效果并重置画布...canvas.restoreToCount(saveCount);// Step 6, draw decorations (foreground, scrollbars) 绘制前景、滚动条等修饰onDrawForeground(canvas);}
        由以上代码可以看得到,系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,而且在第三步和第四步的时候调用了onDraw()方法和dispatchDraw()方法,这样开发者在自定义控件的时候就只需要重写onDraw()方法和dispatchDraw()方法,也就是只需要关注最终显示的内容就可以了,而不需要去绘制其他的修饰。

在View里面的onDraw()方法和dispatchDraw()方法都是空实现,也就是留给开发者去实现里面的具体逻辑。同时,在开发者实现onDraw()方法和dispatchDraw()方法时也可以不用去绘制其他的修饰了。需要说明一点,View中的draw()方法并不是和measure()方法一样被final修饰,draw()方法没有被final修饰,所以是可以重写的,但是当我们重写draw()方法时,必须和系统中View的draw()方法一样,一步一步的实现,否则就不能将控件绘制出来,所以在自定义控件的时候,一般都是重写onDraw()或dispatchDraw()方法。

如果自定义控件是继承至View时,就重写onDraw()方法,在onDraw()方法中绘制的结果就是最终显示的结果。

public class CircleView extends View {    Paint paint;    public CircleView(Context context) {        this(context, null);    }    public CircleView(Context context, AttributeSet attrs) {        super(context, attrs);        paint = new Paint(Paint.ANTI_ALIAS_FLAG);        paint.setColor(Color.RED);        paint.setStyle(Paint.Style.FILL);    }    @Override    protected void onDraw(Canvas canvas) {        canvas.drawCircle(150,150,35,paint);    }}
这段代码就是在界面上以坐标(300,300)绘制了一个半径为100的红色实心圆
Android自定义View之View的绘制流程_第2张图片

如果自定义控件是继承至ViewGroup时,就重写dispatchDraw()方法,这里直接查看系统FrameLayout的dispatchDraw()方法:

@Overrideprotected void dispatchDraw(Canvas canvas) {final int childrenCount = mChildrenCount;...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);}}if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}   ...}
在代码里面调用了drawChild(canvas, child, drawingTime)方法用来绘制孩子:

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

而在drawChild()方法中就是调用了view的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法(三个参数的方法)来绘制自己,如果孩子控件还是ViewGroup的子类,又会重新调用drawChild()方法递归处理,直到所有的孩子控件绘制完成,也就表示控件绘制完成了。其实这也和measure、layout的过程一样,可以看做是一个递归的过程。

看一下view中三个参数的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法:

/** * This method is called by ViewGroup.drawChild() to have each child view draw itself. * 这个方法是在ViewGroup的drawChild()方法中调用,用来绘制每一个孩子控件自身。 * This draw() method is an implementation detail and is not intended to be overridden or * to be called from anywhere else other than ViewGroup.drawChild(). * 这个方法除了在ViewGroup的drawChild()方法中被调用外,不应该在其它任何地方去复写或调用该方法,它属于ViewGroup。 * 而这个方法最终也会调用View的draw(canvas)一个参数的方法来进行绘制。 */boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {...if (!hasDisplayList) {// 调用computeScroll()方法,这个方法是用来与Scroller类结合实现实现动画效果的computeScroll();sx = mScrollX;sy = mScrollY;}... if (!hasDisplayList) {// Fast path for layouts with no backgroundsif ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 继续调用dispatchDraw()方法递归处理dispatchDraw(canvas);} else {// 调用View的draw(canvas)一个参数的方法draw(canvas);}} else {mPrivateFlags &= ~PFLAG_DIRTY_MASK;((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);}...return more;}
了解更多关于Scroller类的相关内容,可以查看《 Android中的Scroller类》这篇博客。

draw总结:

① View绘制的画布canvas是从surface对象中获得,而最终也是绘制到surface中去。surface提供了一个双缓存机制,可以提高绘制的效率;

② 系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,如果在自定义View时直接继承View时是重写draw()方法,就必须和系统中View的draw()方法一样,一步一步的实现,否则就不能将控件展示出来;

③ 因为自定义控件一般重写onDraw()方法,所以每一个控件都会绘制滚动条和其他的修饰;

④ 自定义View如果直接继承制View时,需要重写onDraw()方法,在onDraw()中绘制的内容就是最终展示到界面的内容,自定义View如果是直接继承ViewGroup,那就重写dispatchDraw()方法,绘制ViewGroup的孩子控件;

⑤ Android绘制的过程和measure、layout一样,可以看做是一个递归的过程。


总体来说,View的绘制从开始到结束要经历几个过程:
测量大小,回调 onMeasure()方法
组件定位,回调 onLayout()方法
组件绘制,回调 onDraw()方法

更多相关文章

  1. Android开发之Android自带的下拉刷新控件SwipeRefreshLayout
  2. Google 菜市场(Android Market)上不去的解决方法
  3. Android高手进阶教程(二十一)之---Android中创建与几种解析xml的
  4. Android 应用程序查找设备的方法——以串口为例
  5. Android--自定义视图控件(一)(Android Studio)
  6. android中使用httpclient方法获得网页内容并对json对象解析

随机推荐

  1. s3c2410/2440(armv4t) 移植android
  2. Android对kernel增加的:Early suspend, La
  3. Ice Cream Sandwich 邀请函:10 月 19 号,香
  4. Android使用Service实现简单音乐播放实例
  5. Intent的用法(初步)
  6. 从零开始学编程——环境配置
  7. Android之自定义各种控件
  8. [Android] ProcessBuilder与Runtime.getR
  9. Android(安卓)UI开发第二十九篇――Andro
  10. Android(安卓)Studio Drawable和Mipmap文