自定义控件三部曲之绘图篇(十三)——Canvas与图层(一)

来源:互联网 发布:算法导论 第四版 mobi 编辑:程序博客网 时间:2024/05/21 22:49

前言:猛然知道姥姥79了,我好怕,好想哭

系列文章:

Android自定义控件三部曲文章索引:http://blog.csdn.net/harvic880925/article/details/50995268


在给大家讲解了paint的几个方法之后,我觉得有必要插一篇有关Canvas画布的知识,在开始paint之前,我们讲解了canvas绘图的几篇文章和cavas的save()、store()的知识,这篇是对Canvas的一个系统的补充,前几篇文章链接如下:
《自定义控件之绘图篇(一):概述及基本几何图形绘制》
《 自定义控件之绘图篇(二):路径及文字》
《自定义控件之绘图篇(三):区域(Range)》
《自定义控件之绘图篇(四):canvas变换与操作》

一、如何获得一个Canvas对象

方法一:自定义view时, 重写onDraw、dispatchDraw方法

(1)、定义

我们先来看一下onDraw、dispatchDraw方法的定义

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3. }  
  4.   
  5. protected void dispatchDraw(Canvas canvas) {  
  6.     super.dispatchDraw(canvas);  
  7. }  
可以看到onDraw、dispatchDraw在传入的参数中都有一个canvas对象。这个canvas对象是View中的Canvas对象,利用这个canvas对象绘图,效果会直接反应在View中; 

(2)、onDraw、dispatchDraw区别

  • 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、构建方法

使用:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Canvas c = new Canvas(bitmap);  

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Canvas c = new Canvas();   
  2. c.setBitmap(bitmap);   
其中bitmap可以从图片加载,也可以创建,有下面几种方法
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //方法一:新建一个空白bitmap  
  2. Bitmap bmp = Bitmap.createBitmap(width ,height Bitmap.Config.ARGB_8888);  
  3. //方法二:从图片中加载  
  4. 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才能画到view上。 
下面举个例子:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class BitmapCanvasView extends View {  
  2.     private Bitmap mBmp;  
  3.     private Paint mPaint;  
  4.     private Canvas mBmpCanvas;  
  5.     public BitmapCanvasView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.   
  8.         mPaint = new Paint();  
  9.         mPaint.setColor(Color.RED);  
  10.         mBmp = Bitmap.createBitmap(500 ,500 , Bitmap.Config.ARGB_8888);  
  11.         mBmpCanvas = new Canvas(mBmp);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void onDraw(Canvas canvas) {  
  16.         super.onDraw(canvas);  
  17.   
  18.         mPaint.setTextSize(100);  
  19.         mBmpCanvas.drawText("启舰大SB",0,100,mPaint);  
  20.     }  
  21. }  
我们先看一下运行结果:

可以看到,毛线也没有,这是为什么呢? 
我们仔细来看一下onDraw函数:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.   
  4.     mPaint.setTextSize(100);  
  5.     mBmpCanvas.drawText("启舰大SB",0,100,mPaint);  
  6. }  
在onDraw函数中,我们只是将文字画在了mBmpCanvas上,也就是我们新建mBmp图片上!这个图片跟我们view没有任何关系好吧,我们需要把mBmp图片画到view上才行,所以我们在onDraw中需要加下面这句,将mBmp画到view上
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. canvas.drawBitmap(mBmp,0,0,mPaint);  
所以改造后的代码为:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class BitmapCanvasView extends View {  
  2.     private Bitmap mBmp;  
  3.     private Paint mPaint;  
  4.     private Canvas mBmpCanvas;  
  5.     public BitmapCanvasView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.   
  8.         mPaint = new Paint();  
  9.         mPaint.setColor(Color.RED);  
  10.         mBmp = Bitmap.createBitmap(500 ,500 , Bitmap.Config.ARGB_8888);  
  11.         mBmpCanvas = new Canvas(mBmp);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void onDraw(Canvas canvas) {  
  16.         super.onDraw(canvas);  
  17.   
  18.         mPaint.setTextSize(100);  
  19.         mBmpCanvas.drawText("启舰大SB",0,100,mPaint);  
  20.   
  21.         canvas.drawBitmap(mBmp,0,0,mPaint);  
  22.   
  23.     }  
  24. }  
这时候效果为:

方法三:SurfaceHolder.lockCanvas()

在操作SurfaceView时需要用到Canvas,有关SurfaceView的知识后面会单独开一篇讲解,这里就先过了。

二、图层与画布

《自定义控件之绘图篇(四):canvas变换与操作》中,我们讲过了canvas的save()和restore(),如果没有看过的同学,务必先回去看一遍再回来。 
其实除了save()和restore()以外,还有其它一些函数来保存和恢复画布状态,这部分我们就来看看。

1、saveLayer()

saveLayer()有两个函数:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * 保存指定矩形区域的canvas内容 
  3.  */  
  4. public int saveLayer(RectF bounds, Paint paint, int saveFlags)  
  5. public int saveLayer(float left, float top, float right, float bottom,Paint paint, int saveFlags)  
  • RectF bounds:要保存的区域的矩形。
  • int saveFlags:取值有:ALL_SAVE_FLAG、MATRIX_SAVE_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、CLIP_TO_LAYER_SAVE_FLAG总共有这六个,其中ALL_SAVE_FLAG表示保存全部内容,这些标识的具体意义我们后面会具体讲;
第二个构造函数实际与第一个是一样的,只不过是根据四个点来构造一个矩形。 
下面我们来看一下例子,拿xfermode来做下试验,来看看saveLayer都干了什么:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class XfermodeView extends View {  
  2.     private int width = 400;  
  3.     private int height = 400;  
  4.     private Bitmap dstBmp;  
  5.     private Bitmap srcBmp;  
  6.     private Paint mPaint;  
  7.   
  8.     public XfermodeView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         setLayerType(View.LAYER_TYPE_SOFTWARE, null);  
  12.         srcBmp = makeSrc(width, height);  
  13.         dstBmp = makeDst(width, height);  
  14.         mPaint = new Paint();  
  15.     }  
  16.   
  17.     @Override  
  18.     protected void onDraw(Canvas canvas) {  
  19.         super.onDraw(canvas);  
  20.         canvas.drawColor(Color.GREEN);  
  21.   
  22.         int layerID = canvas.saveLayer(00, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);  
  23.         canvas.drawBitmap(dstBmp, 00, mPaint);  
  24.         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
  25.         canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
  26.         mPaint.setXfermode(null);  
  27.         canvas.restoreToCount(layerID);  
  28.     }  
  29.   
  30.     // create a bitmap with a circle, used for the "dst" image  
  31.     static Bitmap makeDst(int w, int h) {  
  32.         Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
  33.         Canvas c = new Canvas(bm);  
  34.         Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);  
  35.   
  36.         p.setColor(0xFFFFCC44);  
  37.         c.drawOval(new RectF(00, w, h), p);  
  38.         return bm;  
  39.     }  
  40.   
  41.     // create a bitmap with a rect, used for the "src" image  
  42.     static Bitmap makeSrc(int w, int h) {  
  43.         Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
  44.         Canvas c = new Canvas(bm);  
  45.         Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);  
  46.   
  47.         p.setColor(0xFF66AAFF);  
  48.         c.drawRect(00, w, h, p);  
  49.         return bm;  
  50.     }  
  51. }  
这段代码大家应该很熟悉,这是我们在讲解setXfermode()时的示例代码,但在saveLayer前把整个屏幕画成了绿色,效果图如下:

那么问题来了,如果我们把saveLayer给去掉,看看会怎样:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     canvas.drawColor(Color.GREEN);  
  4.     canvas.drawBitmap(dstBmp, 00, mPaint);  
  5.     mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
  6.     canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
  7.     mPaint.setXfermode(null);  
  8. }  
效果图就变这样了:

我擦类……去掉saveLayer()居然效果都不一样了…… 
我们先回顾下Mode.SRC_IN的效果:在处理源图像时,以显示源图像为主,在相交时利用目标图像的透明度来改变源图像的透明度和饱和度。当目标图像透明度为0时,源图像就完全不显示。 
再回过来看结果,第一个结果是对的,因为不与圆相交以外的区域透明度都是0,而第二个图像怎么就变成了这屌样,源图像全部都显示出来了。 

(1)、saveLayer的绘图流程

这是因为在调用saveLayer时,会生成了一个全新的bitmap,这个bitmap的大小就是我们指定的保存区域的大小,新生成的bitmap是全透明的,在调用saveLayer后所有的绘图操作都是在这个bitmap上进行的。 
所以:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. int layerID = canvas.saveLayer(00, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);  
  2. canvas.drawBitmap(dstBmp, 00, mPaint);  
  3. mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
  4. canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
我们讲过,在画源图像时,会把之前画布上所有的内容都做为目标图像,而在saveLayer新生成的bitmap上,只有dstBmp对应的圆形,所以除了与圆形相交之外的位置都是空像素。 
在画图完成之后,会把saveLayer所生成的bitmap盖在原来的canvas上面。 
所以此时的xfermode的合成过程如下图所示:

savelayer新建的画布上的图像做为目标图像,矩形所在的透明图层与之相交,计算结果画在新建的透明画布上。最终将计算结果直接盖在原始画布上,形成最终的显示效果。 

(2)、没有saveLayer的绘图流程

然后我们再来看第二个示例,在第二个示例中,唯一的不同就是把saveLayer去掉了; 
在saveLayer去掉后,所有的绘图操作都放在了原始View的Canvas所对应的Bitmap上了

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. protected void onDraw(Canvas canvas) {  
  2.     super.onDraw(canvas);  
  3.     canvas.drawColor(Color.GREEN);  
  4.     canvas.drawBitmap(dstBmp, 00, mPaint);  
  5.     mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
  6.     canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);  
  7.     mPaint.setXfermode(null);  
  8. }  
由于我们先把整个画布给染成了绿色,然后再画上了一个圆形,所以在应用xfermode来画源图像的时候,目标图像当前Bitmap上的所有图像了,也就是整个绿色的屏幕和一个圆形了。所以这时候源图像的相交区域是没有透明像素的,透明度全是100%,这也就不难解释结果是这样的原因了。 
此时的xfermode合成过程如下:

由于没有调用saveLayer,所以圆形是直接画在原始画布上的,而当矩形与其相交时,就是直接与原始画布上的所有图像做计算的。 
所以有关saveLayer的结论来了:
saveLayer会创建一个全新透明的bitmap,大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap上进行。在绘制结束后,会直接盖在上一层的Bitmap上显示。

2、画布与图层

上面我们讲到了画布(Bitmap)、图层(Layer)和Canvas的概念,估计大家都会被绕晕了;下面我们下面来具体讲解下它们之间的关系。 
图层(Layer):每一次调用canvas.drawXXX系列函数时,都会生成一个透明图层来专门来画这个图形,比如我们上面在画矩形时的透明图层就是这个概念。 
画布(bitmap):每一个画布都是一个bitmap,所有的图像都是画在bitmap上的!我们知道每一次调用canvas.drawxxx函数时,都会生成一个专用的透明图层来画这个图形,画完以后,就盖在了画布上。所以如果我们连续调用五个draw函数,那么就会生成五个透明图层,画完之后依次盖在画布上显示。 
画布有两种,第一种是view的原始画布,是通过onDraw(Canvas canvas)函数传进来的,其中参数中的canvas就对应的是view的原始画布,控件的背景就是画在这个画布上的! 
另一种是人造画布,通过saveLayer()、new Canvas(bitmap)这些方法来人为新建一个画布。尤其是saveLayer(),一旦调用saveLayer()新建一个画布以后,以后的所有draw函数所画的图像都是画在这个画布上的,只有当调用restore()、resoreToCount()函数以后,才会返回到原始画布上绘制。 
Canvas:这个概念比较难理解,我们可以把Canvas理解成画板,Bitmap理解成透明画纸,而Layer则理解成图层;每一个draw函数都对应一个图层,在一个图形画完以后,就放在画纸上显示。而一张张透明的画纸则一层层地叠加在画板上显示出来。我们知道画板和画纸都是用夹子夹在一起的,所以当我们旋转画板时,所有画纸都会跟着旋转!当我们把整个画板裁小时,所以的画纸也都会变小了! 
这一点非常重要,当我们利用saveLayer来生成多个画纸时,然后最上层的画纸调用canvas.rotate(30)是把画板给旋转了,所有的画纸也都被旋转30度!这一点非常注意 
另外,如果最上层的画纸调用canvas.clipRect()将画板裁剪了,那么所有的画纸也都会被裁剪。唯一能够恢复的操作是调用canvas.revert()把上一次的动作给取消掉! 
但在利用canvas绘图与画板不一样的是,画布的影响只体现在以后的操作上,以前画上去的图像已经显示在屏幕上是不会受到影响的。 
这一点一定要理解出来,下面会用到。

三、save()、saveLayer()、saveLayerAlpha()中的用法

1、saveLayer的用法

saveLayer的声明如下:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public int saveLayer(RectF bounds, Paint paint, int saveFlags)  
  2. public int saveLayer(float left, float top, float right, float bottom,Paint paint, int saveFlags)  
我们前面提到了saveLayer会新建一个画布(bitmap),后续的所有操作都是在这个画布上进行的。下面我们来分别看下saveLayer使用中的注意事项 

(1)、saveLayer后的所有动作都只对新建画布有效

我们先看个例子:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class SaveLayerUseExample_3_1 extends View{  
  2.     private Paint mPaint;  
  3.     private Bitmap mBitmap;  
  4.     public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {  
  5.         super(context, attrs);  
  6.         mPaint = new Paint();  
  7.         mPaint.setColor(Color.RED);  
  8.         mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.dog);;  
  9.     }  
  10.   
  11.     @Override  
  12.     protected void onDraw(Canvas canvas) {  
  13.         super.onDraw(canvas);  
  14.         canvas.drawBitmap(mBitmap,0,0,mPaint);  
  15.   
  16.         int layerID = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint,Canvas.ALL_SAVE_FLAG);  
  17.         canvas.skew(1.732f,0);  
  18.         canvas.drawRect(0,0,150,160,mPaint);  
  19.         canvas.restoreToCount(layerID);  
  20.     }  
  21. }  
效果图如下:

在onDraw中,我们先在view的原始画布上画上了小狗的图像,然后利用saveLayer新建了一个图层,然后利用canvas.skew将新建的图层水平斜切45度。所以之后画的矩形(0,0,150,160)就是斜切的。 
而正是由于在新建画布后的各种操作都是针对新建画布来操作的,不会对以前的画布产生影响,从效果图中也明显可以看出,将画布水平斜切45度也只影响了saveLayer的新建画布,并没有对之前的原始画布产生影响。 

(2)、通过Rect指定矩形大小就是新建的画布大小

在saveLayer的参数中,我们可以通过指定Rect对象或者指定四个点来来指定一个矩形,这个矩形的大小就是新建画布的大小,我们举例来看一下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class SaveLayerUseExample_3_1 extends View {  
  2.     private Paint mPaint;  
  3.     private Bitmap mBitmap;  
  4.   
  5.     public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mPaint = new Paint();  
  8.         mPaint.setColor(Color.RED);  
  9.         mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);  
  10.         ;  
  11.     }  
  12.   
  13.     @Override  
  14.     protected void onDraw(Canvas canvas) {  
  15.         super.onDraw(canvas);  
  16.         canvas.drawBitmap(mBitmap, 00, mPaint);  
  17.   
  18.         int layerID = canvas.saveLayer(00100100, mPaint, Canvas.ALL_SAVE_FLAG);  
  19.         canvas.drawRect(00500600, mPaint);  
  20.         canvas.restoreToCount(layerID);  
  21.     }  
  22. }  

效果图如下:


在绘图时,我们先把小狗图片绘制在原始画布上的,然后新建一个大小为(0,0,100,100)大小的透明画布,然后再在这个画布上画一个(0, 0, 500, 600)的矩形。由于画布大小只有(0,0,100,100),所以(0, 0, 500, 600)这个矩形并不能完全显示出来,也只能显示出来(0,0,100,100)画布大小的部分。 
那有些同学会说了,nnd,为了避免画布太小而出现问题,我每次都新建一个屏幕大小的画布多好,这样虽然是不会出现问题,但你想过没有,屏幕大小的画布需要多少空间吗,按一个像素需要8bit存储空间算,1024*768的机器,所使用的bit数就是1024*768*8=6.2M!所以我们在使用saveLayer新建画布时,一定要选择适当的大小,不然你的APP很可能OOM哦。
注意,注意:在我的例子中都是直接新建全屏画布的,因为写例子比较方便!!!!但是我这只是示例,在现实使用中,一定要适量的创建画布的大小哦!

2、saveLayerAlpha的用法

saveLayerAlpha的声明如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)  
  2. public int saveLayerAlpha(float left, float top, float right, float bottom,int alpha, int saveFlags)  
相比saveLayer,多一个alpha参数,用以指定新建画布透明度,取值范围为0-255,可以用16进制的oxAA表示; 
这个函数的意义也是在调用的时候会新建一个bitmap画布,以后的各种绘图操作都作用在这个画布上,但这个画布是有透明度的,透明度就是通过alpha值指定的。 
我们来看个示例
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class SaveLayerAlphaView extends View {  
  2.     private Paint mPaint;  
  3.     public SaveLayerAlphaView(Context context, AttributeSet attrs) {  
  4.         super(context, attrs);  
  5.         mPaint = new Paint();  
  6.         mPaint.setColor(Color.RED);  
  7.     }  
  8.   
  9.     @Override  
  10.     protected void onDraw(Canvas canvas) {  
  11.         super.onDraw(canvas);  
  12.   
  13.         canvas.drawRect(100,100,300,300,mPaint);  
  14.   
  15.         int layerID = canvas.saveLayerAlpha(0,0,600,600,0x88,Canvas.ALL_SAVE_FLAG);  
  16.         mPaint.setColor(Color.GREEN);  
  17.         canvas.drawRect(200,200,400,400,mPaint);  
  18.         canvas.restoreToCount(layerID);  
  19.   
  20.     }  
  21. }  
效果图如下:

在saveLayerAlpha以后,我们画了一个绿色的矩形,由于把saveLayerAlpha新建的矩形的透明度是0x88(136)大概是50%透明度,从效果图中也可以看出在新建图像与上一画布合成后,是具有透明度的。

好了,这篇文章就先到这里,下一篇详细给大家讲解有关参数中各个Flag的意义。

如果本文有帮到你,记得加关注哦

源码下载地址:http://download.csdn.net/detail/harvic880925/9510189 (与第二篇对应资源合并在一个工程中)

请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/51317746 谢谢

0 0