转载 https://www.gcssloop.com/customview/Canvas_Convertcss
原本想把画布操做放到后面部分的,可是发现不少图形绘制都离不开画布操做,因而先讲解一下画布的基本操做方法。java
操做类型 | 相关API | 备注 |
---|---|---|
绘制颜色 | drawColor, drawRGB, drawARGB | 使用单一颜色填充整个画布 |
绘制基本形状 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧 |
绘制图片 | drawBitmap, drawPicture | 绘制位图和图片 |
绘制文本 | drawText, drawPosText, drawTextOnPath | 依次为 绘制文字、绘制文字时指定每一个文字位置、根据路径绘制文字 |
绘制路径 | drawPath | 绘制路径,绘制贝塞尔曲线时也须要用到该函数 |
顶点操做 | drawVertices, drawBitmapMesh | 经过对顶点操做可使图像形变,drawVertices直接对画布做用、 drawBitmapMesh只对绘制的Bitmap做用 |
画布剪裁 | clipPath, clipRect | 设置画布的显示区域 |
画布快照 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数 |
画布变换 | translate, scale, rotate, skew | 依次为 位移、缩放、 旋转、错切 |
Matrix(矩阵) | getMatrix, setMatrix, concat | 实际上画布的位移,缩放等操做的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些经常使用的方法。 |
画布操做能够帮助咱们用更加容易理解的方式制做图形。canvas
例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么作?网络
按照咱们一般的想法(被常年训练出来的数学思惟),就是先使用三角函数计算出线段结束点的坐标,而后调用drawLine便可。函数
然而这是不是被固有思惟禁锢了?oop
假设咱们先绘制一个长度为20dp的水平线,而后将这条水平线旋转30度,则最终看起来效果是相同的,并且不用进行三角函数计算,这样是否更加简单了一点呢?post
合理的使用画布操做能够帮助你用更容易理解的方式创做你想要的效果,这也是画布操做存在的缘由。spa
PS: 全部的画布操做都只影响后续的绘制,对以前已经绘制过的内容没有影响。翻译
translate是坐标系的移动,能够为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动,以下:3d
// 省略了建立画笔的代码 // 在坐标原点绘制一个黑色圆形 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);
咱们首先将坐标系移动一段距离绘制一个圆形,以后再移动一段距离绘制一个圆形,两次移动是可叠加的。
缩放提供了两个方法,以下:
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);
为了效果明显,此次我不只添加了坐标系并且对矩形中几个重要的点进行了标注,具备相同字母标注的点是一一对应的。
因为本次未对缩放中心进行偏移,全部默认的缩放中心就是坐标原点,中心轴就是x轴和y轴。
本次缩放能够看作是先根据缩放中心(坐标原点)缩放到原来的0.5倍,而后分别按照x轴和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);
添加了这么多的辅助内容,但愿你们可以看懂。
本次对缩放中心点y轴坐标进行了偏移,故中心轴也向右偏移了。
PS:和位移(translate)同样,缩放也是能够叠加的。
canvas.scale(0.5f,0.5f); canvas.scale(0.5f,0.1f);
调用两次缩放则 x轴实际缩放为0.5x0.5=0.25 y轴实际缩放为0.5x0.1=0.05
下面咱们利用这一特性制做一个有趣的图形。
注意设置画笔模式为描边(STROKE)
// 将坐标系原点移动到画布正中心 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); }
旋转提供了两种方法:
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);
调用两次旋转,则实际的旋转角度为180+20=200度。
为了演示这一个效果,我作了一个不明觉厉的东西:
// 将坐标系原点移动到画布正中心 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); }
skew这里翻译为错切,错切是特殊类型的线性变换。
错切只提供了一种方法:
public void skew (float sx, float sy)
参数含义:
float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.
变换后:
X = x + sx * y
Y = sy * x + y
示例:
// 将坐标系原点移动到画布正中心 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); // 水平错切 <- 45度 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); // 水平错切 canvas.skew(0,1); // 垂直错切 mPaint.setColor(Color.BLUE); // 绘制蓝色矩形 canvas.drawRect(rect,mPaint);
Q: 为何存在快照与回滚
A:画布的操做是不可逆的,并且不少画布操做会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而由于坐标系的移动绘制出来的实际位置不一样。因此会对画布的一些状态进行保存和回滚。
与之相关的API:
相关API | 简介 |
---|---|
save | 把当前的画布的状态进行保存,而后放入特定的栈中 |
saveLayerXxx | 新建一个图层,并放入特定的栈中 |
restore | 把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布 |
restoreToCount | 弹出指定位置及其以上全部的状态,并按照指定位置的状态进行恢复 |
getSaveCount | 获取栈中内容的数量(即保存次数) |
下面对其中的一些概念和方法进行分析:
其实这个栈我也不知道叫什么名字,暂时叫作状态栈吧,它看起来像下面这样:
这个栈能够存储画布状态和图层状态。
Q:什么是画布和图层?
A:实际上咱们看到的画布是由多个图层构成的,以下图(图片来自网络):
实际上咱们以前讲解的绘制操做和画布操做都是在默认图层上进行的。
在一般状况下,使用默认图层就可知足需求,可是若是须要绘制比较复杂的内容,如地图(地图能够有多个地图层叠加而成,好比:政区层,道路层,兴趣点层)等,则分图层绘制比较好一些。
你能够把这些图层看作是一层一层的玻璃板,你在每层的玻璃板上绘制内容,而后把这些玻璃板叠在一块儿看就是最终效果。
名称 | 简介 |
---|---|
ALL_SAVE_FLAG | 默认,保存所有状态 |
CLIP_SAVE_FLAG | 保存剪辑区 |
CLIP_TO_LAYER_SAVE_FLAG | 剪裁区做为图层保存 |
FULL_COLOR_LAYER_SAVE_FLAG | 保存图层的所有色彩通道 |
HAS_ALPHA_LAYER_SAVE_FLAG | 保存图层的alpha(不透明度)通道 |
MATRIX_SAVE_FLAG | 保存Matrix信息( translate, rotate, scale, skew) |
save 有两种方法:
// 保存所有状态 public int save () // 根据saveFlags参数保存一部分状态 public int save (int saveFlags)
能够看到第二种方法比第一种多了一个saveFlags参数,使用这个参数能够只保存一部分状态,更加灵活,这个saveFlags参数具体可参考上面表格中的内容。
每调用一次save方法,都会在栈顶添加一条状态信息,以上面状态栈图片为例,再调用一次save则会在第5次上面载添加一条状态。
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方法进行恢复。
这个暂时不过多讲述,若是之后用到详细讲解。(由于这里面东西也有很多啊QAQ)
状态回滚,就是从栈顶取出一个状态而后根据内容进行恢复。
一样以上面状态栈图片为例,调用一次restore方法则将状态栈中第5次取出,根据里面保存的状态进行状态恢复。
弹出指定位置以及以上全部状态,并根据指定位置状态进行恢复。
以上面状态栈图片为例,若是调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。
获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为5。
不过请注意,该函数的最小返回值为1,即便弹出了全部的状态,返回值依旧为1,表明默认状态。
虽然关于状态的保存和回滚啰嗦了很多,不过大多数状况下只须要记住下面的步骤就能够了:
save(); //保存状态 ... //具体操做 restore(); //回滚到以前的状态
这种方式也是最简单和最容易理解的使用方法。
如本文一开始所说,合理的使用画布操做能够帮助你用更容易理解的方式创做你想要的效果。