Android View绘制(四)onDraw过程与Canvas Bitmap
draw()方法
经过对View
的测量与布局过程后,下面就到了真正的View
绘制的过程了。这个过程从调用根View
的draw()
方法开始:(省略部分代码)
1 | public void draw(Canvas canvas) { |
这段源码来自于View
,过程非常清晰,执行了以下的步骤(如果需要):
- 27-30行进行判断是否跳过注释中的第2、5步,通常情况跳过
- 22-24行,进行背景的绘制
- 32行,调用
onDraw()
方法进行自身的绘制 - 35行,调用
dispatchDraw()
方法,进行子View
的绘制(调用子View
的draw()
方法),同时也表明了子View
的绘制在自身之后这一顺序 - 43行,进行前景的绘制,一般为装饰组件,如滚动条等
dispatchDraw()方法
onDraw()
方法先不谈,看看dispatchDraw()
方法做了什么,以ViewGroup
为例:(省略部分代码)
1 |
|
简单来看,依次调用了子View
的draw()
方法。所以对于有子View
的ViewGroup
, 我们需要重写这个方法来决定子View
绘制的顺序。
Canvas Bitmap Surface间的联系
背景与前景绘制的过程一般不由我们控制,自定义View
时关键的内容就在onDraw()
方法中。
你可能已经发现,在这些View
绘制过程中的函数都具有一个参数Canvas
,这个Canvas
字面意义上为画布,那它实际上是什么,又在绘制过程中起着什么样的重要作用呢?
我们可以把Canvas
看作是系统给予我们的一个虚拟的对象,或者说是我们绘制图形的一个中介,Canvas
具有一系列的方法可以供我们调用来直观地绘制图形,我们对于Canvas
的所有操作都会被系统处理从而反映在屏幕上而不用我们去手动地决定哪一个像素应该显示什么颜色。
在Canvas
背后则是一个Bitmap
对象,我们的绘制实际上会反映在这个Bitmap
上再交由系统来显示。如果我们需要自己创建一个Canvas
,我们必须创建一个Bitmap
对象作为Canvas
的构造参数。例如:
1 | Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); |
这样这个Canvas
就会在指定的Bitmap
上进行绘制,我们也可以通过Canvas
的drawBitmap()
方法来在指定的Bitmap
上绘制。
那么问题来了,onDraw()
方法给我们提供的这个Canvas
是从哪里来的,为什么我们对它的操作可以反映到屏幕上?下面这张图便于我们去了解这个过程:
可以看到,我们的屏幕被分为了几个Window
,每一个Window
都有着自己的Surface
,Surface
具有两级缓存,每个缓存中存放着将要显示在屏幕上的像素数据,而当我们想要刷新屏幕显示新的内容时,对应的Surface
将会读取缓存中的数据来进行更新。
onDraw()
方法中的canvas
就是在一个Surface
显示完毕,将这个Surface
锁定时由它返回的,在这个canvas
上进行的操作就可以在下一次刷新屏幕时显示,但是实际上并不是由canvas
直接写数据到它的Surface
缓存中,这中间还有一个对象就是我们之前提到的Bitmap
,Bitmap
储存着的正是像素信息,而Surface
返回的canvas
中含有的就是一个指向Surface
缓存的Bitmap
。
梳理一下整个过程,我们需要做的是操作这个封装了一系列绘图方法的canvas
,canvas
将操作反映到内含的Bitmap
上,Bitmap
将数据反映给Surface
的缓存,Surface
在下一次刷新时读取缓存中的内容并显示到屏幕上。
这里还应注意的是每个Window
有且仅有一个单继承(即只有一个根)的View
树,View
将会将Surface
返回的canvas
向下传递来让子View
依次完成部分区域的绘制。
弄清楚这个canvas
的来源之后,我们就可以放心地在用它来“作画”了。
onDraw()方法
在onDraw()
方法中我们可以对方法参数提供的canvas
进行操作,绘制各种自定义的图形。
我们可以选择一个现有的View
作为自定义View
的父类,在它的onDraw()
方法中一定要调用super.onDraw()
来令它绘制本来的组件,我们可以在调用super.onDraw()
之前或之后插入我们自己的代码,这取决你对绘制顺序的需要。
注意有些View
如Linearlayout
默认是不绘制自己的,也就是说它们并不会调用onDraw()
方法,当我们需要继承这类View
来进行自定义并进行绘制的话需要调用setWillNotDraw(false);
。可以在onMeasure()
方法中调用。
另一种方式是继承于View
,可以更为自由地订制各种行为。
Canvas
中封装了非常多的方法,下面列举一部分:
- [drawArc](https://developer.android.com/reference/android/graphics/Canvas.html#drawArc(float, float, float, float, float, float, boolean, android.graphics.Paint))(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint):绘制椭圆
- [drawCircle](https://developer.android.com/reference/android/graphics/Canvas.html#drawCircle(float, float, float, android.graphics.Paint))(float cx, float cy, float radius, Paint paint):绘制圆形
- drawColor(int color):对整个
Canvas
填充颜色 - [drawLine](https://developer.android.com/reference/android/graphics/Canvas.html#drawLine(float, float, float, float, android.graphics.Paint))(float startX, float startY, float stopX, float stopY, Paint paint):绘制直线
- [drawLines](https://developer.android.com/reference/android/graphics/Canvas.html#drawLines(float[], int, int, android.graphics.Paint))(float[] pts, int offset, int count, Paint paint):绘制一系列直线
- [drawPicture](https://developer.android.com/reference/android/graphics/Canvas.html#drawPicture(android.graphics.Picture, android.graphics.Rect))(Picture picture, Rect dst):绘制一张图片
- [drawPoint](https://developer.android.com/reference/android/graphics/Canvas.html#drawPoint(float, float, android.graphics.Paint))(float x, float y, Paint paint):绘制一个点
- [drawPoints](https://developer.android.com/reference/android/graphics/Canvas.html#drawPoints(float[], int, int, android.graphics.Paint))(float[] pts, int offset, int count, Paint paint):绘制一系列点
- [drawRect](https://developer.android.com/reference/android/graphics/Canvas.html#drawRect(float, float, float, float, android.graphics.Paint))(float left, float top, float right, float bottom, Paint paint):绘制矩形
- [drawRoundRect](https://developer.android.com/reference/android/graphics/Canvas.html#drawRoundRect(android.graphics.RectF, float, float, android.graphics.Paint))(RectF rect, float rx, float ry, Paint paint):绘制圆角矩形
- [drawText](https://developer.android.com/reference/android/graphics/Canvas.html#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint))(CharSequence text, int start, int end, float x, float y, Paint paint):绘制文字
- [drawTextOnPath](https://developer.android.com/reference/android/graphics/Canvas.html#drawTextOnPath(java.lang.String, android.graphics.Path, float, float, android.graphics.Paint))(String text, Path path, float hOffset, float vOffset, Paint paint):沿路径绘制文字
方法的详细信息在官方文档中。
我们注意到,许多绘制方法都需要一个Paint
参数, 这个Paint
可以理解为系统为我们抽象出的一支画笔,我们所绘制的图形都是用这支画笔绘制出来的,当然因此我们就可以对画笔设置颜色、粗细等属性,我们甚至可以用setShader()
方法为这个Paint
设置一个Shader
,来实现各种特殊的动态效果,Shader
的使用需要另起一篇博客来讲。