安卓自定义 View 进阶:Canvas 之画布操作

上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。

本来想把画布操作放到后面部分的,但是发现很多图形绘制都离不开画布操作,于是先讲解一下画布的基本操作方法。

一.Canvas的常用操作速查表

操作类型 相关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基本操作

1.画布操作

为什么要有画布操作?

画布操作可以帮助我们用更加容易理解的方式制作图形。

例如: 从坐标原点为起点,绘制一个长度为20dp,与水平线夹角为30度的线段怎么做?

按照我们通常的想法(被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用drawLine即可。

然而这是否是被固有思维禁锢了?

假设我们先绘制一个长度为20dp的水平线,然后将这条水平线旋转30度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。


⑴位移(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);

我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的。


⑵缩放(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);

为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是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

下面我们利用这一特性制作一个有趣的图形。

  • // 将坐标系原点移动到画布正中心
  • 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);
  • }


⑶旋转(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);

改变旋转中心位置:

top Created with Sketch.