Android View绘制(四)onDraw过程与Canvas Bitmap

draw()方法

经过对View测量布局过程后,下面就到了真正的View绘制的过程了。这个过程从调用根Viewdraw()方法开始:(省略部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* 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;
}

这段源码来自于View,过程非常清晰,执行了以下的步骤(如果需要):

  1. 27-30行进行判断是否跳过注释中的第2、5步,通常情况跳过
  2. 22-24行,进行背景的绘制
  3. 32行,调用onDraw()方法进行自身的绘制
  4. 35行,调用dispatchDraw()方法,进行子View的绘制(调用子Viewdraw()方法),同时也表明了子View的绘制在自身之后这一顺序
  5. 43行,进行前景的绘制,一般为装饰组件,如滚动条等

dispatchDraw()方法

onDraw()方法先不谈,看看dispatchDraw()方法做了什么,以ViewGroup为例:(省略部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
}
}

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

简单来看,依次调用了子Viewdraw()方法。所以对于有子ViewViewGroup, 我们需要重写这个方法来决定子View绘制的顺序。

Canvas Bitmap Surface间的联系

背景与前景绘制的过程一般不由我们控制,自定义View时关键的内容就在onDraw()方法中。

你可能已经发现,在这些View绘制过程中的函数都具有一个参数Canvas,这个Canvas字面意义上为画布,那它实际上是什么,又在绘制过程中起着什么样的重要作用呢?

我们可以把Canvas看作是系统给予我们的一个虚拟的对象,或者说是我们绘制图形的一个中介,Canvas具有一系列的方法可以供我们调用来直观地绘制图形,我们对于Canvas的所有操作都会被系统处理从而反映在屏幕上而不用我们去手动地决定哪一个像素应该显示什么颜色。

Canvas背后则是一个Bitmap对象,我们的绘制实际上会反映在这个Bitmap上再交由系统来显示。如果我们需要自己创建一个Canvas,我们必须创建一个Bitmap对象作为Canvas的构造参数。例如:

1
2
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

这样这个Canvas就会在指定的Bitmap上进行绘制,我们也可以通过CanvasdrawBitmap()方法来在指定的Bitmap上绘制。

那么问题来了,onDraw()方法给我们提供的这个Canvas是从哪里来的,为什么我们对它的操作可以反映到屏幕上?下面这张图便于我们去了解这个过程:

canvas_bitmap

可以看到,我们的屏幕被分为了几个Window,每一个Window都有着自己的SurfaceSurface具有两级缓存,每个缓存中存放着将要显示在屏幕上的像素数据,而当我们想要刷新屏幕显示新的内容时,对应的Surface将会读取缓存中的数据来进行更新。

onDraw()方法中的canvas就是在一个Surface显示完毕,将这个Surface锁定时由它返回的,在这个canvas上进行的操作就可以在下一次刷新屏幕时显示,但是实际上并不是由canvas直接写数据到它的Surface缓存中,这中间还有一个对象就是我们之前提到的BitmapBitmap储存着的正是像素信息,而Surface返回的canvas中含有的就是一个指向Surface缓存的Bitmap

梳理一下整个过程,我们需要做的是操作这个封装了一系列绘图方法的canvascanvas将操作反映到内含的Bitmap上,Bitmap将数据反映给Surface的缓存,Surface在下一次刷新时读取缓存中的内容并显示到屏幕上。

这里还应注意的是每个Window有且仅有一个单继承(即只有一个根)的View树,View将会将Surface返回的canvas向下传递来让子View依次完成部分区域的绘制。

弄清楚这个canvas的来源之后,我们就可以放心地在用它来“作画”了。

onDraw()方法

onDraw()方法中我们可以对方法参数提供的canvas进行操作,绘制各种自定义的图形。

我们可以选择一个现有的View作为自定义View的父类,在它的onDraw()方法中一定要调用super.onDraw()来令它绘制本来的组件,我们可以在调用super.onDraw()之前或之后插入我们自己的代码,这取决你对绘制顺序的需要。

注意有些ViewLinearlayout默认是不绘制自己的,也就是说它们并不会调用onDraw()方法,当我们需要继承这类View来进行自定义并进行绘制的话需要调用setWillNotDraw(false);。可以在onMeasure()方法中调用。

另一种方式是继承于View,可以更为自由地订制各种行为。

Canvas中封装了非常多的方法,下面列举一部分:

方法的详细信息在官方文档中。

我们注意到,许多绘制方法都需要一个Paint参数, 这个Paint可以理解为系统为我们抽象出的一支画笔,我们所绘制的图形都是用这支画笔绘制出来的,当然因此我们就可以对画笔设置颜色、粗细等属性,我们甚至可以用setShader()方法为这个Paint设置一个Shader,来实现各种特殊的动态效果,Shader的使用需要另起一篇博客来讲。