一、获取Canvas对象的方法
方法一:重写onDraw()、dispatchDraw()函数
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
protected void dispatchDraw(Canvas canvas) {
super.dispatch(canvas);
}
可以看到,onDraw()、dispatchDraw()函数在传入的参数中都有一个Canvas对象,这个Canvas对象是View中的Canvas对象,利用这个Canvas对象绘图,效果会直接反映在View中。
区别:
● onDraw()函数用于绘制视图自身。
● dispatchDraw()函数用于绘制子视图。
无论是View还是ViewGroup,对这两个函数的调用顺序都是onDraw()→dispatchDraw()。
但在ViewGroup中,当它有背景的时候就会调用onDraw()函数;否则就会跳过onDraw()函数,直接调用dispatchDraw()函数。所以,如果要在ViewGroup中绘图,则往往会重写dispatchDraw()函数。
在View中,onDraw()和dispatchDraw()函数都会被调用,所以无论我们把绘图代码放在onDraw()还是dispatchDraw()函数中都是可以得到效果的。但是,由于dispatchDraw()函数用于绘制子视图,所以,从原则上来讲,在绘制View控件时,我们会重写onDraw()函数。
总结:在绘制View控件时,需要重写onDraw()函数;在绘制ViewGroup控件时,需要重写dispatchDraw()函数。
方法二:使用Bitmap创建
1.构建方法
Canvas c = new Canvas(bitmap);
或:
Canvas c = new Canvas();
c.setBitmap(bitmap);
其中,bitmap可以从图片中加载,也可以自行创建。
// 方法一:新建一个空白bitmap
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// 方法二:从图片中加载
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.wave_bg, null);
除这两种方法以外,还有其他几种方法(比如构造一个具有Matrix的图像副本——前面示例中的图片倒影),这里就不再涉及了,可以去官网查看 Bitmap 的构造函数 。
2.在onDraw()函数中使用
需要注意的是,如果我们用Bitmap构造一个Canvas,那么在这个Canvas上绘制的图像也都会保存在这个Bitmap上,而不会画在View上。如果想画在View上,就必须使用onDraw(Canvas canvas)函数传入的Canvas画一遍Bitmap。
public class BitmapCanvasView extends View {
private Bitmap mBmp;
private Paint mPaint;
private Canvas mBmpCanvas;
public BitmapCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mBmp = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
mBmpCanvas = new Canvas(mBmp);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setTextSize(50);
mBmpCanvas.drawText("欢迎光临", 0, 100, mPaint);
// canvas.drawBitmap(mBmp, 0, 0, mPaint);
}
}
运行这段代码后会发现,结果是一片空白,我们写的字去哪儿了?在onDraw()函数中,我们只是将文字写在了mBmpCanvas上,也就是我们新建的mBmp图片上,而最终没有将图片画在画布上。因为文字被写在了图片上,而画布上却没有任何内容,所以结果是一片空白。如果将注释掉的最后一句打开,即可将图片画在画布上,在视图上就会显示文字了。
方法三:调用SurfaceHolder.lockCanvas()函数
在使用SurfaceView时,当调用SurfaceHolder.lockCanvas()函数时,也会创建Canvas对象,有关SurfaceView知识在下一章有讲解。
二、图层与画布
前面讲过Canvas的save()和restore()函数,除这两函数以外,还有其他一些函数用来保存和恢复画布状态。
saveLayer()函数:
/**
* 保存指定矩形区域的Canvas内容
*/
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags)
● bounds:要保存的区域所对应的矩形对象
● saveFlags:取值有ALL_SAVE_FLAG、MATRIX_SAVA_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、CLIP_TO_LAYER_SAVE_FLAG,其中ALL_SAVE_FLAG表示保存全部内容。
public class CustomView extends View {
private int width = 400;
private int height = 400;
private Bitmap dstBmp;
private Bitmap srcBmp;
private Paint mPaint;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
srcBmp = makeSrc(width, height);
dstBmp = makeDst(width, height);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); // 算法:[Sa*Da, Sc*Da]
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerID);
}
//创建一张圆形图片
private Bitmap makeDst(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas C = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFFFFCC44);
C.drawOval(new RectF(0, 0, w, h), p);
return bm;
}
//创建一张矩形图片
private Bitmap makeSrc(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFF66AAFF);
c.drawRect(0, 0, w, h, p);
return bm;
}
}
显示正常。但如果把saveLayer()函数去掉,则会怎样?
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
// int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); // 算法:[Sa*Da, Sc*Da]
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
mPaint.setXfermode(null);
// canvas.restoreToCount(layerID);
}
这时源图像矩形怎么全部显示出来了?
1.调用saveLayer()函数时的绘图流程
在调用saveLayer()函数时,会生成一块全新的画布(Bitmap),这块画布的大小就是我们指定的所要保存区域的大小。新生成的画布是全透明的,在调用savaLayer()函数后所有的绘图操作都是在这块画布上进行的。
在利用Xfermode画源图像时,会把之前画布上所有的内容都作为目标图像;而在调用saveLayer()函数新生成的画布上,只有dstBmp对应的圆形。所以,在Mode.SRC_IN模式下,除与圆形相交之外的位置都是空白像素。
对于Xfermode而言,在绘图完成之后,会把调用saveLayer()函数所生成的透明画布覆盖在原来的画布上面,以形成最终的显示结果。
此时的Xfermode的合成过程如下图所示。
中间的透明画布就是调用saveLayer()函数自动生成的,最上方的透明图层是调用drawBitmap()函数生成的。
每次调用canvas.drawXXX系列函数,都会生成一个透明图层专门绘制这个图形,而每次生成的图层都会叠加到最近的画布上。
因为在这里对源图像(矩形)应用了Xfermode算法,所以在叠加到就近的调用saveLayer()函数生成的画布上时,会进行计算。在新建的画布上绘制完成以后,整体覆盖在原始画布上显示出来。
正是因为在使用Xfermode计算时,目标图像是绘制在新建的透明画布上的,所以除圆形以外的区域全部是透明像素,最终的显示结果是正确的。
2.没有saveLayer()函数时的绘图流程
在去掉saveLayer()函数后,就不会新建画布了。当然,所有的绘图操作都会在原始画布上进行。
由于先把整块画布染成了绿色,再画一个圆形,所以在应用Xfermode来画源图像时,在目标画布上是没有透明像素的。所以,当矩形与其相交时,就是直接与原始画布上的所有图像进行计算的。结果也就是那样的。
结论:调用saveLayer()函数会创建一块全新的透明画布,大小与指定的区域大小一致,其后的绘图操作都放在这块画布上进行。在绘制结束后,会直接覆盖在原始画布上显示。
画布与图层:
画布(Bitmap)、图层(Layer)、Canvas,这三者之间的关系:
● 图层(Layer):每次调用canvas.drawXXX系列函数,都会生成一个透明图层专门来绘制这个图形,比如前面在绘制矩形时的透明图层就是这个概念。
● 画布(Bitmap):每块画布都是一个Bitmap,所有的图像都是画在这个Bitmap上的。我们知道,每次调用canvas.drawXXX系列函数,都会生成一个专用的透明图层来绘制这个图形,绘制完成以后,就覆盖在画布上。所以,如果我们连续调用5个draw函数,就会生成5个透明图层,画完之后依次覆盖在画布上显示。画布有两种:一种是View的原始画布,是通过onDraw(Canvas canvas)函数传入的,参数中的canvas对应的是View的原始画布,控件的背景就是画在这块画布上的;另一种是人造画布,通过saveLayer()、newCanvas(bitmap)等函数来人为地新建一块画布。尤其是saveLayer()函数,一旦调用saveLayer()函数新建一块画布,以后所有draw函数所画的图像都是画在这块画布上的,只有在调用restore()、resoreToCount()函数以后,才会返回到原始画布上进行绘制。
● Canvas:Canvas是画布的表现形式,我们所要绘制的任何东西都是利用Canvas来实现的。在代码中,Canvas的生成方式只有一种——new Canvas(bitmap),即只能通过Bitmap生成,无论是原始画布还是人造画布,所有的画布最后都是通过Canvas画到Bitmap上的。可以把Canvas理解成绘图的工具,利用它所封装的绘图函数来绘图,而所要绘制的内容最后是画在Bitmap上的。所以,如果我们利用Canvas.clipXXX系列函数将画布进行裁剪,其实就是把它对应的Bitmap进行裁剪,与之对应的结果是以后再利用Canvas绘图的区域会减小。
saveLayer()和saveLayerAlpha()函数的用法:
1.saveLayer()函数的用法
saveLayer()函数会新建一块画布(Bitmap),后续的所有操作都是在这块画布上进行的。
使用saveLayer()函数注意事项:
(1)saveLayer()函数后的所有动作都只对新建画布有效。
public class SaveLayerUseExample extends View {
private Paint mPaint;
private Bitmap mBitmap;
public SaveLayerUseExample(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight() ,mPaint,Canvas.ALL SAVE FLAG);
canvas.skew(1.732f, 0);// 将新建图层水平倾斜60°(tan60°=√3)
canvas.drawRect(0, 0, 150, 160, mPaint);
canvas.restoreToCount(layerID);
}
}
/**
* @params sx 将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,其实就是将y逆时针旋转相应的角度
* @params sy 将画布在y方向上倾斜相应的角度,sx倾斜角度的tan值,其实就是将x顺时针旋转相应的角度
*/
public void skew(float sx, float sy)
在onDraw()函数中,我们先在View的原始画布上画上了小狗图像,然后利用saveLayer()函数新建了一个图层,接着利用canvas.skew()函数将新建的图层水平斜切60°,所以之后画的矩形(0,0,150,160)就是倾斜的。
而正是由于在新建画布后的各种操作都是针对新建画布进行的,所以不会对以前的画布产生影响。从效果图中也可以明显看出,将画布水平倾斜60°只影响了saveLayer()函数的新建画布,并没有对原始画布产生影响。
(2)通过Rect指定的矩形大小就是新建的画布大小。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Paint mPaint = new Paint();
// 如果指定drawBitmap的第二个参数Rect对象,则是截取mBitmap部分填充到目标Rect区域
canvas.drawBitmap(mBitmap, null, new Rect(0, 0, 700, 600), mPaint);
int layerID = canvas.saveLayer(0, 0, 200, 200, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawColor(Color.GRAY);
canvas.restoreToCount(layerID);
}
在绘图时 ,我们先把小狗图像绘制在原始画布上,然后新建一个大小为(0,0,200,200)的透明画布,并将画布填充为灰色。由于画布大小只有(0,0,200,200),所以从效果图中可 以看出,也只有这一小部分区域被填充为灰色。
可能会想,为了避免画布太小而出现问题,每次新建一块屏幕大小的画布不就好了?
这样做虽然不会 出现问题,但屏幕大小的画布需要多少存储空间呢?按一个像素需要 8bit 存储空间算,分辨率为1024像素×768像素的机器,所占用的存储空间就是1024x768×8=6.2MB。所以我们在使用saveLayer()函数新建画布时,一定要选择适当的大小,否则你的APP很可能OOM(Out Of Memory,内存溢出)。
2.saveLayerAlpha()函数的用法
public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int saveFlags)
相比于saveLayer()函数就是多了一个透明度参数,alpha取值0~255,可以用十六进制0xAA表示。这样创建的画布具有透明度。
三、Flag的具体含义
在Canvas中有如下几个save系列函数:
public int save()
public int save(int saveFlags)
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayerAlpha(RectF bounds, Paint paint, int saveFlags)
先考虑一下:如果让我们保存一块画布的状态,以便恢复,则需要保存哪些内容呢?
第一个是位置信息,第二个是大小信 息,好像除此之外也没什么了。位置信息对应的是MATIX_SAVE_FLAG,大小信息对应的是 CLIP_SAVE_FLAG,这是save()和saveLayer()函数所公用的标识。而saveLayer()函数专用的三个标识用于指定saveLayer()函数新建的画布具有哪种特性,而不是保存画布的范畴 。
事实上,在API 26,即Android8.0开始,FLAG就只有一个了,并且还是默认值。
官方原文:
Generally ALL_SAVE_FLAG is recommended for performance reasons. Value is either 0 or ALL_SAVE_FLAG.
As of API Level API level android.os.Build.VERSION_CODES.P Build.VERSION_CODES.P the only valid saveFlags is ALL_SAVE_FLAG. All other flags are ignored.
即,save()/saveLayer()/saveLayerAlpha()从Android8.0开始,已经不带有最后一个参数:int saveFlags了。连官方文档里面都已经没有带参的save()方法了。而且到了Android9.0唯有ALL_SAVE_FLAG是有效的,其他的都将被忽略。
所以,这里不去讲解其他的_SAVE_FLAG了。
canvas.translate(平移)、canvas.rotate(旋转)、canvas.scale(缩放)、canvas.skew(扭曲)其实都是利用位置矩阵Matrix实现的,可利用save()、restore()保存状态。
save()函数与saveLayer()函数的区别在于:saveLayer()函数会新建一块画布,而save()函数不会新建画布。
save()用来保存Canvas的状态,save()方法之后的代码,可以调用Canvas的平移、放缩、旋转、裁剪等操作。
restore()用来恢复Canvas之前保存的状态(可以想成是保存坐标轴的状态),防止save()方法代码之后对Canvas执行的操作,继续对后续的绘制会产生影响,通过该方法可以避免连带的影响。
public class MATRIX_SAVE_FLAG_View extends View {
private Paint mPaint;
public MATRIX_SAVE_FLAG_View(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE ,null);
mPaint = new Paint();
mPaint.setColor(Color.GRAY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.rotate(40);
canvas.drawRect(l00, 0, 200, 100, mPaint);
canvas.restore();
mPaint.setColor(Color.BLACK);
canvas.drawRect(l00, 0, 200, 100, mPaint);
}
}
先调用canvas.save()函数将Canvas的所有标识都保存起来(当然包括位置矩阵);然后将画布旋转 40°,画一个灰色矩形;接着调用canvas.restore()函数将画布恢复;最后在同一个位置画一个黑色矩形。
可分为4步:
➀canvas.save()保存了当前画布的所有状态,特别是坐标轴状态这个位置矩阵,状态命名为statusOrigin。
➁对canvas进行平移、旋转、缩放、扭曲等操作。
➂canvas.restore()让画布回到了当初save()时的状态:statusOrign。要注意在恢复画布状态之前,在画布上所绘制的所有一切是仍存在的!
➃画布在状态:statusOrign上进行系列操作。
四、恢复画布
恢复画布有两个函数:restore()与restoreToCount()
restore():把回退栈中的最上层画布状态出栈,恢复画布状态。
restoreToCount(int count):
在save()、saveLayer()、saveLayerAlpha()函数保存画布后,都会返回一个ID值,这个ID值表示当前保存的画布信息的栈层索引(从0开始)。比如,保存在第三层,则返回2。
public void restoreToCount(int saveCount);
它表示一直退栈,直到把指定索引的画布信息退出来,之后的栈最上层的画布信息将作为最新的画布。
public class CustomView extends View {
private Paint mPaint;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int id1 = canvas.save();
canvas.clipRect(0, 0, 600, 600);
canvas.drawColor(Color.RED);
Log.d("TAG", "count:" + canvas.getSaveCount() + " id1:" + id1);
int id2 = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.clipRect(100, 100, 500, 500);
canvas.drawColor(Color.GREEN);
Log.d("TAG", "count:" + canvas.getSaveCount() + " id2:" + id2);
int id3 = canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 0xf0, Canvas.ALL_SAVE_FLAG);
canvas.clipRect(200, 200, 400, 400);
canvas.drawColor(Color.YELLOW);
Log.d("TAG", "count:" + canvas.getSaveCount() + " id3:" + id3);
int id4 = canvas.save();
canvas.clipRect(250, 250, 350, 350);
canvas.drawColor(Color.BLUE);
Log.d("TAG", "count:" + canvas.getSaveCount() + " id4:" + id4);
// canvas.restoreToCount(id3);
// canvas.drawColor(Color.GRAY);
// Log.d(TAG,"count:"+canvas.getSaveCount());
}
}
打印日志:
D/TAG: count:2 id1:1
D/TAG: count:3 id2:2
D/TAG: count:4 id3:3
D/TAG: count:5 id4:4
解注释最后三行代码运行。
D/TAG: count:2 id1:1
D/TAG: count:3 id2:2
D/TAG: count:4 id3:3
D/TAG: count:5 id4:4
D/TAG: count:3
从代码和日志可以看出,在调用canvas.restoreToCount(id3)函数后,将恢复到生成id3之前的画布状态,id3之前的画布状态就是(100,100,500,500)。
restore()与restoreToCount(int count)的关系:
这两个函数针对的是同一个栈,所以完全可以通用。不同的是,restore()函数默认将栈顶内容退出还原画布;而restoreToCount(int count)函数则一直退栈,直到把指定索引的画布信息退出来,之后的栈最上层的画布信息将作为最新的画布(即成为当前画布)。
结论:
(1)restore()的含义是把回退栈中的最上层画布状态出栈,恢复画布状态。restoreToCount(int count)的含义是一直退栈,直到把指定索引的画布信息退出来,将此之前的所有动作都恢复。
(2)无论哪种save函数、哪个Flag,保存画布时使用的都是同一个栈。
(3)restore()与restoreToCount(int count)针对的是同一个栈,所以完全可以通用。