自定义控件之绘图篇(四) —— Canvas变换与操作

前言

平移(translate)

Canvas中有一个函数translate()是用来实现画布平移的,画布的原状是以左上角为原点,向左是X轴正方向,向下是Y轴正方向,如下图所示

这里写图片描述

translate()其实实现的相当于平移坐标系,即平移坐标系的原点的位置。translate()的原型如下:

void translate(float dx, float dy)

参数说明:

  • dx:水平方向平移的距离,正数指向正(右)方向平移的量,负数为向负(左)方向平移的量

  • 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);  
}  

上面这段代码,先把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系列函数),都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的:

  • 调用canvas.drawRect(rect1, paint_green)时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0),再在系统Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:

这里写图片描述

  • 然后再第二次调用canvas.drawRect(rect1, paint_red)时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:合成视图,从上往下看的合成方式。

这里写图片描述

上图展示了,上层的Canvas图层与底部的屏幕的合成过程,由于Canvas画布已经平移了100像素,所以在画图时是以新原点来产生视图的,然后合成到屏幕上,这就是我们上面最终看到的结果了。我们看到屏幕移动之后,有一部分超出了屏幕的范围,那超出范围的图像显不显示呢,当然不显示了!也就是说,Canvas上虽然能画上,但超出了屏幕的范围,是不会显示的。当然,我们这里也没有超出显示范围,两框框而已。

下面对上面的知识做一下总结:

  1. 每次调用canvas.drawXXXX系列函数来绘图进,都会产生一个全新的Canvas画布

  2. 如果在drawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的!每次产生的画布的最新位置都是这些操作后的位置(关于save()restore()的画布可逆问题的后面再讲)。

  3. Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的

旋转(Rotate)

画布的旋转是默认是围绕坐标原点来旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实我们旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。其实Rotate函数有两个构造函数:

  • 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)

其实我也没弄懂第二个构造函数是怎么使用的,我就先讲讲第一个构造函数的参数吧:

  • sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n*sx,所以,sx为小数为缩小,sx为整数为放大

  • 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)

参数说明

  • sx:将画布在X方向上倾斜相应的角度,sx倾斜角度的tan

  • sy:将画布在Y轴方向上倾斜相应的角度,sy为倾斜角度的tan

变换后为

  • x = x + sx * y

  • y = sy * x + y

注意,这里全是倾斜角度的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系列函数,通过与RectPathRegion取交、并、差等集合运算来获得最新的画布形状。除了调用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、PathRegion来取得最新画布的函数,难度都不大,就不再一一讲述。利用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))操作,并将当前画布画成白色背景,也就是上图中最小块的白色部分,是最后的当前画布。

如果现在使用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);  
    //保存画布大小为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基本使用

原文链接:点击这里

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值