目录
一、简介
Canvas类中的方法大致可以分为三类:
- drawXXX 等一系列绘制相关的方法;
- scale、rotate、clipXXX 等对画布进行操作的方法;
- save、restore 等与层的保存和回滚相关的方法;
前面几篇介绍了drawXXX系列的方法,本篇继续分析scale、rotate、clipXXX 等对画布进行操作的方法。
1、为什么要有画布操作?
画布操作可以帮助我们用更加容易理解的方式制作图形。
例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?
按照我们通常的想法,就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。
然而利用对画布的操作可以用更加简单的方式实现。
假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算。
合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。
PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。
二、Canvas的基本操作
Canvas的操作一般分为四类:translate(平移)、scale(缩放)、rotate(旋转)、skew(错切)。
在分析之前先明确下几个基本概念:在Android中,默认的坐标零点位于屏幕左上角,向下为Y轴正方向,向右为X轴正方向,反之为负。
1、Translate(平移)
canvas类提供的方法:
public void translate(float dx, float dy)
参数说明:
- float dx:X坐标的偏移量
- float dy:Y坐标的偏移量
先在屏幕画一个矩形:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
效果图:
在将Canvas进行平移操作后绘制同样坐标的矩形,为了对比效果重新设置画笔颜色:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
//向X轴方向移动200,向Y轴方向移动200
canvas.translate(200,200);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
效果:
然后在将Canvas进行同样的平移操作:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
//向X轴方向移动200,向Y轴方向移动200
canvas.translate(200,200);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
//向X轴方向移动200,向Y轴方向移动200
canvas.translate(200,200);
mPaint.setColor(Color.RED);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
效果:
从效果上看,两次translate 进行了叠加,绘制第二个矩形的时候画布已经偏移了(400,400);即:Translate是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动。
2、Scale(缩放)
Canvas提供了两个方法进行scale:
public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)
参数说明:
- float sx:X轴缩放比例
- float sy:Y轴缩放比例
- float px:缩放中心点的x坐标
- float py:缩放中心点的y坐标
仔细观察会发现scale是以坐标原点为中心,沿着坐标轴方向进行的。如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
//以原点(0,0)为中心,x、y坐标分别缩小0.5
canvas.scale(0.5f,0.5f);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
效果图:
当(sx,sy)取值大于1时是按比例放大,a小于1时是按比例缩小;取值小于0时,则会翻转后进行缩放,如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.you2);
canvas.translate(300,300);
canvas.drawBitmap(mBitmap,0,0,null);
canvas.scale(-0.5f,-0.5f);
canvas.drawBitmap(mBitmap,0,0,null);
}
效果图:
下面看第二个方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
//以原点(800,800)为中心,x、y坐标分别缩小0.5
canvas.scale(0.5f,0.5f,800,800);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
效果图:
以上就是以红点(800,800)为原点的进行缩放,其实通过源码能看出它的实现原理:
/**
* Preconcat the current matrix with the specified scale.
*
* @param sx The amount to scale in X
* @param sy The amount to scale in Y
* @param px The x-coord for the pivot point (unchanged by the scale)
* @param py The y-coord for the pivot point (unchanged by the scale)
*/
public final void scale(float sx, float sy, float px, float py) {
if (sx == 1.0f && sy == 1.0f) return;
translate(px, py);
scale(sx, sy);
translate(-px, -py);
}
可以看出第二个方法是通过先将Canvas平移一段距离(px,py)出进行缩放后在平移相同的距离回来(-px,-py),但是由于叠加效果,实际上最后的平移的距离是经过缩放的。下面按照源码的原理实现下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
canvas.drawPoint(800,800,mPaint);
canvas.translate(800,800);
canvas.scale(0.5f,0.5f);
canvas.translate(-800,-800);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
看下效果图,发现效果和scale(0.5f,0.5f,800,800)相同:
3、Rotate(旋转)
同样,Canvas提供了两个方法进行rotate:
public void rotate (float degrees)
public final void rotate (float degrees, float px, float py)
参数说明:
- float degrees:旋转角度
- float px:旋转中心点的x坐标
- float py:旋转中心点的x坐标
看下实现代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.you2);
canvas.drawBitmap(mBitmap,0,0,null);
canvas.rotate(30);
canvas.drawBitmap(mBitmap,0,0,null);
}
效果图:
第二个方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.you2);
canvas.drawBitmap(mBitmap,0,0,null);
canvas.drawPoint(500,500,mPaint);
canvas.rotate(180,500,500);
canvas.drawBitmap(mBitmap,0,0,null);
}
效果图:
同理,旋转的操作也是可以叠加的。
4、skew(错切)
错切是特殊类型的线性变换。Canvas为skew提供了一个方法:
public void skew (float sx, float sy)
参数说明:
- float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
- float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.
(x,y)坐标经过skew变换后:
- X = x + sx * y
- Y = sy * x + y
看下实现代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.you2);
canvas.drawBitmap(mBitmap,0,0,null);
canvas.skew(0.5f,0.5f);
canvas.drawBitmap(mBitmap,0,0,null);
}
效果图:
同理,错切也是可叠加的,不过请注意:调用次序不同绘制结果也会不同。
5、save(快照)和restore(回滚)
Q: 为什存在快照与回滚?
A:画布的操作是不可逆的叠加的,很多画布操作会影响后续的步骤。所以会对画布的一些状态进行保存和回滚。即:
- save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
- restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
注意:save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。save和restore之间,往往夹杂的是对Canvas的特殊操作。
如同上面Translate中的两个平移操作在没有使用save和restore的情况下是叠加的,效果图如下:
在代码中上save和restore操作,代码如下,效果图在上面右边。可以看出此时平移的操作并没有叠加,因为Canvas的状态被回滚到上次save的时候。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
canvas.save();
//向X轴方向移动200,向Y轴方向移动200
canvas.translate(200,200);
mPaint.setColor(Color.GRAY);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
canvas.restore();
//向X轴方向移动200,向Y轴方向移动200
canvas.translate(200,200);
mPaint.setColor(Color.RED);
canvas.drawRect(new RectF(0,0,400,400), mPaint);
}
Canvas提供的save方法有两种,如下:
// 保存全部状态
public int save ()
// 根据saveFlags参数保存一部分状态
public int save (int saveFlags)
参数说明:
- int saveFlags:标志位,指定要保存/恢复的Canvas状态的哪些部分
saveFlags取值和其意义如下:
FLAG | 意义 |
---|---|
ALL_SAVE_FLAG | 保存所有的标识 |
MATRIX_SAVE_FLAG | 仅保存canvas的matrix数组 |
CLIP_SAVE_FLAG | 仅保存canvas的当前大小 |
注意:save (int saveFlags)在官方文档中已经被标记过时不推荐使用了,用save()方法代替。
6、clipXXX(裁剪)
Canvas提供了三种裁剪的方式:
- clipRect:裁剪一个矩形,在矩形内绘制。
- clipPath:裁剪Path包括的范围,Path所包括的范围不是空的才有效。
- clipOutRect、clipOutPath:要求API版本最小26,暂不作说明。
6.1、clipRect
对于clipRect,Canvas提供了多个重载方法,如下:
public boolean clipRect (Rect rect)
public boolean clipRect (RectF rect)
public boolean clipRect (int left, int top, int right, int bottom)
public boolean clipRect (float left, float top, float right, float bottom)
public boolean clipRect (Rect rect, Region.Op op)
public boolean clipRect (RectF rect, Region.Op op)
public boolean clipRect (float left, float top, float right, float bottom, Region.Op op)
从上面这些重载方法可以看出,clipRect函数的参数主要是两个部分:
裁剪区域(Rect,或者传入的Rect的四个坐标):这部分参数很好理解,直接看代码。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);
canvas.clipRect(new RectF(100,100,400,400));
canvas.drawColor(Color.RED);
}
效果图:红色部分是裁剪后的区域。
Region.Op:从源码中可以看出其是一个枚举类,
public enum Op {
DIFFERENCE(0),
INTERSECT(1),
UNION(2),
XOR(3),
REVERSE_DIFFERENCE(4),
REPLACE(5);
Op(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
Region是区域的意思。而Region.op里面的值其实表示就是传入的区域与当前区域的组合方式:
- DIFFERENCE(0)://取当前区域中去除与传入区域重叠的部分
- INTERSECT(1): //取传入区域与当前区域的重叠的部分
- UNION(2), //合并,即传入区域与当前区域的并集部分
- XOR(3), //异或操作,即传入区域与当前区域不重叠的部分
- REVERSE_DIFFERENCE(4), //取传入区域中去除与当前区域重叠的部分
- REPLACE(5); //取传入的区域
如图表示:
6.2、clipPath
Canvas提供两个clipPath重载方法:
public boolean clipPath(Path path,Region.Op op)
public boolean clipPath(Path path)
上面介绍过Region.Op参数的意义,简单实现下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path path = new Path();
path.addCircle(400, 400, 200, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawColor(Color.RED);
}
效果图:
ps:clipPath有明显的锯齿,没有找到好的办法解决,所以使用时注意下。
7、saveLayer(图层)
saveLayer可以为canvas创建一个新的透明图层,在新的图层上绘制,并不会直接绘制到屏幕上,而会在restore之后,绘制到上一个图层或者屏幕上(如果没有上一个图层)。为什么会需要一个新的图层,例如在处理xfermode的时候,原canvas上的图(包括背景)会影响src和dst的合成,这个时候,使用一个新的透明图层是一个很好的选择。又例如需要当前绘制的图形都带有一定的透明度,那么创建一个带有透明度的图层,也是一个方便的选择。可以把这些图层看做是一层一层的玻璃板,在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。如图:
在canvas中提供了以下方法来实现图层操作:
public int saveLayer(RectF bounds, Paint paint)
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom, Paint paint)
public int saveLayer(float left, float top, float right, float bottom, Paint paint,int saveFlags)
//saveLayerAlpha可以开启一个带有透明度的图层
public int saveLayerAlpha(RectF bounds, int alpha)
public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int saveFlags)
参数说明:
- int alpha:取值0-255,表示图层的透明度。
- RectF bounds、folat left、folat top、folat right、folat bottom:矩形和坐标值表示Layer的大小和范围的
- int saveFlags:表示需要保存哪方面的内容,一共有六个值:
MATRIX_SAVE_FLAG,
CLIP_SAVE_FLAG,
HAS_ALPHA_LAYER_SAVE_FLAG,
FULL_COLOR_LAYER_SAVE_FLAG,
CLIP_TO_LAYER_SAVE_,
ALL_SAVE_FLAG。//保存全部
ps:在官方文档中表示saveFlags参数的六个值前面五个都被标记过时不推荐使用了,默认使用ALL_SAVE_FLAG。
代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(100, 100, 100, mPaint);
mPaint.setColor(Color.RED);
canvas.saveLayerAlpha(100, 100, 200, 200, 120, Canvas.ALL_SAVE_FLAG);
canvas.drawCircle(200, 200, 100, mPaint);
canvas.restore();
}
效果图:
三、总结
本文介绍一些操作画布的方法。合理的使用画布操作可以更容易实现想要的效果。
祝:工作顺利!
参考资料:
http://www.gcssloop.com/customview/Canvas_Convert