前言:前几篇讲解了有关canvas绘图的一些操作,今天更深入一些,讲讲对画布的操作,这篇文章不像前几篇那么容易理解,如果以前没有接触过画布的童鞋可能比较难以理解,为什么会这样。我尽量多画图,让大家更清晰明白。
前几天偶然看到一篇文章,写的朴实无华,充满正能量,我非常喜欢里面的一句话,很像我现在的状态,分享给大家。
人生最纠结的事情不是你甘于平淡,而是你明明不希望平凡却不知道未来应该怎么办。 ----摘自《三十岁那年,我的梦想是年薪十万》
相关文章:
1、《android Graphics(一):概述及基本几何图形绘制》
2、《android Graphics(二):路径及文字》
3、《android Graphics(三):区域(Range)》
4、《android Graphics(四):canvas变换与操作》
一、平移(translate)canvas中有一个函数translate()是用来实现画布平移的,画布的原状是以左上角为原点,向左是X轴正方向,向下是Y轴正方向,如下图所示
translate函数其实实现的相当于平移坐标系,即平移坐标系的原点的位置。translate()函数的原型如下:
void translate(float dx, float dy)
参数说明:
float dx:水平方向平移的距离,正数指向正方向(向右)平移的量,负数为向负方向(向左)平移的量
flaot dy:垂直方向平移的距离,正数指向正方向(向下)平移的量,负数为向负方向(向上)平移的量
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); //translate 平移,即改变坐标系原点位置 Paint paint = new Paint(); paint.setColor(Color.GREEN); paint.setStyle(Style.FILL); // canvas.translate(100, 100); Rect rect1 = new Rect(0,0,400,220); canvas.drawRect(rect1, paint); }
1、上面这段代码,先把canvas.translate(100, 100);注释掉,看原来矩形的位置,然后打开注释,看平移后的位置,对比如下图:
未平移 平移后
二、屏幕显示与Canvas的关系很多童鞋一直以为显示所画东西的改屏幕就是Canvas,其实这是一个非常错误的理解,比如下面我们这段代码:
这段代码中,同一个矩形,在画布平移前画一次,平移后再画一次,大家会觉得结果会怎样?
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); //构造两个画笔,一个红色,一个绿色 Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 3); Paint paint_red = generatePaint(Color.RED, Style.STROKE, 3); //构造一个矩形 Rect rect1 = new Rect(0,0,400,220); //在平移画布前用绿色画下边框 canvas.drawRect(rect1, paint_green); //平移画布后,再用红色边框重新画下这个矩形 canvas.translate(100, 100); canvas.drawRect(rect1, paint_red); } private Paint generatePaint(int color,Paint.Style style,int width) { Paint paint = new Paint(); paint.setColor(color); paint.setStyle(style); paint.setStrokeWidth(width); return paint; }代码分析:
这段代码中,对于同一个矩形,在平移画布前利用绿色画下矩形边框,在平移后,再用红色画下矩形边框。大家是不是会觉得这两个边框会重合?实际结果是这样的。
从到这个结果大家可能会狠蛋疼,我第一次看到这个结果的时候蛋都碎一地了要。淡定……
这个结果的关键问题在于,为什么绿色框并没有移动?
这是由于屏幕显示与Canvas根本不是一个概念!Canvas是一个很虚幻的概念,相当于一个透明图层(用过PS的同学应该都知道),每次Canvas画图时(即调用Draw系列函数),都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的:
1、调用canvas.drawRect(rect1, paint_green);时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0);再在系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:
2、然后再第二次调用canvas.drawRect(rect1, paint_red);时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:(合成视图,从上往下看的合成方式)
上图展示了,上层的Canvas图层与底部的屏幕的合成过程,由于Canvas画布已经平移了100像素,所以在画图时是以新原点来产生视图的,然后合成到屏幕上,这就是我们上面最终看到的结果了。我们看到屏幕移动之后,有一部分超出了屏幕的范围,那超出范围的图像显不显示呢,当然不显示了!也就是说,Canvas上虽然能画上,但超出了屏幕的范围,是不会显示的。当然,我们这里也没有超出显示范围,两框框而已。
下面对上面的知识做一下总结:
1、每次调用canvas.drawXXXX系列函数来绘图进,都会产生一个全新的Canvas画布。
2、如果在DrawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的!每次产生的画布的最新位置都是这些操作后的位置。(关于Save()、Restore()的画布可逆问题的画布再讲)
3、在Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的。
三、旋转(Rotate)画布的旋转是默认是围绕坐标原点来旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实我们旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。其实Roate函数有两个构造函数:
void rotate(float degrees)
void rotate (float degrees, float px, float py)
第一个构造函数直接输入旋转的度数,正数是顺时针旋转,负数指逆时针旋转,它的旋转中心点是原点(0,0)
第二个构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py)
下面以第一个构造函数为例,旋转一个矩形,先画出未旋转前的图形,然后再画出旋转后的图形;
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); Paint paint_green = generatePaint(Color.GREEN, Style.FILL, 5); Paint paint_red = generatePaint(Color.RED, Style.STROKE, 5); Rect rect1 = new Rect(300,10,500,100); canvas.drawRect(rect1, paint_red); //画出原轮廓 canvas.rotate(30);//顺时针旋转画布 canvas.drawRect(rect1, paint_green);//画出旋转后的矩形 }效果图是这样的:
这个最终屏幕显示的构造过程是这样的:
下图显示的是第一次画图合成过程,此时仅仅调用canvas.drawRect(rect1, paint_red); 画出原轮廓
然后是先将Canvas正方向依原点旋转30度,然后再与上面的屏幕合成,最后显示出我们的复合效果。
有关Canvas与屏幕的合成关系我觉得我已经讲的够详细了,后面的几个操作Canvas的函数,我就不再一一讲它的合成过程了。
四、缩放(scale )public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)
其实我也没弄懂第二个构造函数是怎么使用的,我就先讲讲第一个构造函数的参数吧
float sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n*sx;所以,sx为小数为缩小,sx为整数为放大
float sy:垂直方向伸缩的比例,同样,小数为缩小,整数为放大
注意:这里有X、Y轴的密度的改变,显示到图形上就会正好相同,比如X轴缩小,那么显示的图形也会缩小。一样的。
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); // //scale 缩放坐标系密度 Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5); Paint paint_red = generatePaint(Color.RED, Style.STROKE, 5); Rect rect1 = new Rect(10,10,200,100); canvas.drawRect(rect1, paint_green); canvas.scale(0.5f, 1); canvas.drawRect(rect1, paint_red); }
五、扭曲(skew)其实我觉得译成斜切更合适,在PS中的这个功能就差不多叫斜切。但这里还是直译吧,大家都是这个名字。看下它的构造函数:
void skew (float sx, float sy)
参数说明:
float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值,
注意,这里全是倾斜角度的tan值哦,比如我们打算在X轴方向上倾斜60度,tan60=根号3,小数对应1.732
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); //skew 扭曲 Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5); Paint paint_red = generatePaint(Color.RED, Style.STROKE, 5); Rect rect1 = new Rect(10,10,200,100); canvas.drawRect(rect1, paint_green); canvas.skew(1.732f,0);//X轴倾斜60度,Y轴不变 canvas.drawRect(rect1, paint_red); }
五、裁剪画布(clip系列函数)裁剪画布是利用Clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用Save、Restore函数以外,这个操作是不可逆的,一但Canvas画布被裁剪,就不能再被恢复!
Clip系列函数如下:
boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)
boolean clipRect(Rect rect, Region.Op op)
boolean clipRect(RectF rect, Region.Op op)
boolean clipRect(int left, int top, int right, int bottom)
boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(RectF rect)
boolean clipRect(float left, float top, float right, float bottom, Region.Op op)
boolean clipRect(Rect rect)
boolean clipRegion(Region region)
boolean clipRegion(Region region, Region.Op op)
以上就是根据Rect、Path、Region来取得最新画布的函数,难度都不大,就不再一一讲述。利用ClipRect()来稍微一讲。
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.RED); canvas.clipRect(new Rect(100, 100, 200, 200)); canvas.drawColor(Color.GREEN); }先把背景色整个涂成红色。显示在屏幕上
然后裁切画布,最后最新的画布整个涂成绿色。可见绿色部分,只有一小块,而不再是整个屏幕了。
关于两个画布与屏幕合成,我就不再画图了,跟上面的合成过程是一样的。
六、画布的保存与恢复(save()、restore())前面我们讲的所有对画布的操作都是不可逆的,这会造成很多麻烦,比如,我们为了实现一些效果不得不对画布进行操作,但操作完了,画布状态也改变了,这会严重影响到后面的画图操作。如果我们能对画布的大小和状态(旋转角度、扭曲等)进行实时保存和恢复就最好了。
这小节就给大家讲讲画布的保存与恢复相关的函数——Save()、Restore()。
int save ()
void restore()
这两个函数没有任何的参数,很简单。
Save():每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中;
restore():每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
为了更清晰的显示这两个函数的作用,下面举个例子:
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.RED); //保存当前画布大小即整屏 canvas.save(); canvas.clipRect(new Rect(100, 100, 800, 800)); canvas.drawColor(Color.GREEN); //恢复整屏画布 canvas.restore(); canvas.drawColor(Color.BLUE); }他图像的合成过程为:(最终显示为全屏幕蓝色)
下面我通过一个多次利用Save()、Restore()来讲述有关保存Canvas画布状态的栈的概念:代码如下:
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.RED); //保存的画布大小为全屏幕大小 canvas.save(); canvas.clipRect(new Rect(100, 100, 800, 800)); canvas.drawColor(Color.GREEN); //保存画布大小为Rect(100, 100, 800, 800) canvas.save(); canvas.clipRect(new Rect(200, 200, 700, 700)); canvas.drawColor(Color.BLUE); //保存画布大小为Rect(200, 200, 700, 700) canvas.save(); canvas.clipRect(new Rect(300, 300, 600, 600)); canvas.drawColor(Color.BLACK); //保存画布大小为Rect(300, 300, 600, 600) canvas.save(); canvas.clipRect(new Rect(400, 400, 500, 500)); canvas.drawColor(Color.WHITE); }
显示效果为:
在这段代码中,总共调用了四次Save操作。上面提到过,每调用一次Save()操作就会将当前的画布状态保存到栈中,所以这四次Save()所保存的状态的栈的状态如下:
注意在,第四次Save()之后,我们还对画布进行了canvas.clipRect(new Rect(400, 400, 500, 500));操作,并将当前画布画成白色背景。也就是上图中最小块的白色部分,是最后的当前的画布。
如果,现在使用Restor(),会怎样呢,会把栈顶的画布取出来,当做当前画布的画图,试一下:
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.RED); //保存的画布大小为全屏幕大小 canvas.save(); canvas.clipRect(new Rect(100, 100, 800, 800)); canvas.drawColor(Color.GREEN); //保存画布大小为Rect(100, 100, 800, 800) canvas.save(); canvas.clipRect(new Rect(200, 200, 700, 700)); canvas.drawColor(Color.BLUE); //保存画布大小为Rect(200, 200, 700, 700) canvas.save(); canvas.clipRect(new Rect(300, 300, 600, 600)); canvas.drawColor(Color.BLACK); //保存画布大小为Rect(300, 300, 600, 600) canvas.save(); canvas.clipRect(new Rect(400, 400, 500, 500)); canvas.drawColor(Color.WHITE); //将栈顶的画布状态取出来,作为当前画布,并画成***背景 canvas.restore(); canvas.drawColor(Color.YELLOW); }上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为***
那如果我连续Restore()三次,会怎样呢?
我们先分析一下,然后再看效果:Restore()三次的话,会连续出栈三次,然后把第三次出来的Canvas状态当做当前画布,也就是Rect(100, 100, 800, 800),所以如下代码:
protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); canvas.drawColor(Color.RED); //保存的画布大小为全屏幕大小 canvas.save(); canvas.clipRect(new Rect(100, 100, 800, 800)); canvas.drawColor(Color.GREEN); //保存画布大小为Rect(100, 100, 800, 800) canvas.save(); canvas.clipRect(new Rect(200, 200, 700, 700)); canvas.drawColor(Color.BLUE); //保存画布大小为Rect(200, 200, 700, 700) canvas.save(); canvas.clipRect(new Rect(300, 300, 600, 600)); canvas.drawColor(Color.BLACK); //保存画布大小为Rect(300, 300, 600, 600) canvas.save(); canvas.clipRect(new Rect(400, 400, 500, 500)); canvas.drawColor(Color.WHITE); //连续出栈三次,将最后一次出栈的Canvas状态作为当前画布,并画成***背景 canvas.restore(); canvas.restore(); canvas.restore(); canvas.drawColor(Color.YELLOW); }结果为:
OK啦,这篇就到了啦。
扩展阅读:《Android 2D Graphics学习(二)、Canvas篇1、Canvas基本使用》
本文所涉及的代码集合,下载地址为:
请大家尊重原创者版权,转载请标明出处: 谢谢!