自定义控件系列的读书笔记,整理自下列资料,不代表博主个人观点 :
GcsSloop/AndroidNote
五、画布操作
为什么要有画布操作?画布操作可以帮助我们用更加容易理解的方式制作图形。
例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。然而这是否是被固有思维禁锢了?假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。
PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。
5.1 位移(translate)
translate是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,如下:
// 在坐标原点绘制一个黑色圆形
mPaint.setColor(Color.BLACK);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);
// 在坐标原点绘制一个蓝色圆形
mPaint.setColor(Color.BLUE);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);
我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的。
5.2 缩放(scale)
缩放提供了两个方法,如下:
public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)
这两个方法中前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。(可以理解为翻转和缩放坐标轴,比较形象)
缩放比例(sx,sy)取值范围详解:
取值范围(n) | 说明 |
---|---|
[-∞, -1) | 先根据缩放中心放大n倍,再根据中心轴进行翻转 |
-1 | 根据缩放中心轴进行翻转 |
(-1, 0) | 先根据缩放中心缩小到n,再根据中心轴进行翻转 |
0 | 不会显示,若sx为0,则宽度为0,不会显示,sy同理 |
(0, 1) | 根据缩放中心缩小到n |
1 | 没有变化 |
(1, +∞) | 根据缩放中心放大n倍 |
如果在缩放时稍微注意一下就会发现缩放的中心默认为坐标原点,而缩放中心轴就是坐标轴,如下:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.scale(0.5f,0.5f); // 画布缩放
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
接下来我们使用第二种方法让缩放中心位置稍微改变一下,如下:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.scale(0.5f,0.5f,200,0); // 画布缩放 <-- 缩放中心向右偏移了200个单位
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.scale(-0.5f,-0.5f); // 画布缩放
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的,本次缩放可以看做是先根据缩放中心(坐标原点)缩放到原来的0.5倍,然后分别按照x轴和y轴进行翻转。
下面再看一个例子,本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.scale(-0.5f,-0.5f,200,0); // 画布缩放 <-- 缩放中心向右偏移了200个单位
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
PS:和位移(translate)一样,缩放也是可以叠加的。
canvas.scale(0.5f,0.5f);
canvas.scale(0.5f,0.1f);
调用两次缩放则 x轴实际缩放为0.5×0.5=0.25 y轴实际缩放为0.5×0.1=0.05
下面我们利用这一特性制作一个有趣的图形。
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(-400,-400,400,400); // 矩形区域
for (int i=0; i<=20; i++) {
canvas.scale(0.9f,0.9f);
canvas.drawRect(rect,mPaint);
}
5.3 旋转(rotate)
旋转提供了两种方法:
public void rotate (float degrees)
public final void rotate (float degrees, float px, float py)
和缩放一样,第二种方法多出来的两个参数依旧是控制旋转中心点的。默认的旋转中心依旧是坐标原点:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.rotate(180); // 旋转180度 <-- 默认旋转中心为原点
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
改变旋转中心位置:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,-400,400,0); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.rotate(180,200,0); // 旋转180度 <-- 旋转中心向右偏移200个单位
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
好吧,旋转也是可叠加的:
canvas.rotate(180);
canvas.rotate(20);
为了演示这一个效果,下面来看一个不明觉厉的东西:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
canvas.drawCircle(0,0,400,mPaint); // 绘制两个圆形
canvas.drawCircle(0,0,380,mPaint);
for (int i=0; i<=360; i+=10){ // 绘制圆形之间的连接线
canvas.drawLine(0,380,0,400,mPaint);
canvas.rotate(10);
}
5.4 倾斜(skew)
skew这里翻译为倾斜,有的地方也叫错切。
/**
* float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
* float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.
*/
public void skew (float sx, float sy)
实例:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,0,200,200); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.skew(1,0); // 在x轴倾斜45度 <-- tan45 = 1
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
倾斜也是可叠加的,不过请注意,调用次序不同绘制结果也会不同:
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);
RectF rect = new RectF(0,0,200,200); // 矩形区域
mPaint.setColor(Color.BLACK); // 绘制黑色矩形
canvas.drawRect(rect,mPaint);
canvas.skew(1,0); // 在x轴倾斜45度 <-- tan45 = 1
canvas.skew(0,1); // 在y轴倾斜45度 <-- tan45 = 1
mPaint.setColor(Color.BLUE); // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);
5.5 快照(save)和回滚(restore)
画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。
相关API | 简介 |
---|---|
save | 把当前的画布的状态进行保存,然后放入特定的栈中 |
saveLayerXxx | 新建一个图层,并放入特定的栈中 |
restore | 把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布 |
restoreToCount | 弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复 |
getSaveCount | 获取栈中内容的数量(即保存次数) |
(1)状态栈
这个栈可以存储画布状态和图层状态。实际上我们看到的画布是由多个图层构成的。可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。
(2)常量:SaveFlags
数据类型 | 名称 | 简介 |
---|---|---|
int | ALL_SAVE_FLAG | 默认,保存全部状态 |
int | CLIP_SAVE_FLAG | 保存剪辑区 |
int | CLIP_TO_LAYER_SAVE_FLAG | 剪裁区作为图层保存 |
int | FULL_COLOR_LAYER_SAVE_FLAG | 保存图层的全部色彩通道 |
int | HAS_ALPHA_LAYER_SAVE_FLAG | 保存图层的alpha(不透明度)通道 |
int | MATRIX_SAVE_FLAG | 保存Matrix信息(translate, rotate, scale, skew) |
(3)save方法
save 有两种方法:
// 保存全部状态
public int save ()
// 根据saveFlags参数保存一部分状态
public int save (int saveFlags)
可以看到第二种方法比第一种多了一个saveFlags参数,使用这个参数可以只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。
每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。
(4)saveLayerXxx方法
// 无图层alpha(不透明度)通道
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)
// 有图层alpha(不透明度)通道
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)
注意:saveLayerXxx方法会让你花费更多的时间去渲染图像(图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。
使用saveLayerXxx方法,也会将图层状态也放入状态栈中,同样使用restore方法进行恢复。
(5)restore方法
状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。
(6)restoreToCount方法
弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。
以上面状态栈图片为例,如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。
(7)getSaveCount方法
获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。
不过请注意,该函数的最小返回值为1,即使弹出了所有的状态,返回值依旧为1,代表默认状态。
(8)常用姿势
save(); //保存状态
... //具体操作(可以更改画布状态)
restore(); //回滚到之前的状态