在 Android 开发中,Canvas 是自定义 View 绘制的核心工具,而 变换(Transformations) 让我们可以灵活地操控图像,创造丰富的视觉效果。这些变换包括 旋转(rotate)、缩放(scale) 还是 平移(translate),合理地使用这些变换不仅可以优化绘制流程,还能减少资源消耗。
在本文中,我们将深入探讨 Canvas 变换的工作原理,分析 Matrix 矩阵的底层计算逻辑,并通过实际案例用动画帮助你掌握 Canvas 变换。
缩放、旋转、平移的单个变换
既然要讲解 Canvas 变换,我们就先从最简单的单个变换开始,然后逐步深入对单个变换进行各种组合。
在这篇文章中,我将以下面的这个600*600 的正方形图片为例进行变换,通过这个正方形图片能够很明显地展示出变换是如何进行的:
这里注意背景的网格线,都是以 200 为间隔,通过这些网格线能更方便地进行观察。另外,红色的线则是X轴和Y轴,并且将原点从左上角移动到了两条红色线交点的位置。
下面我们就从单个变换开始,再逐步深入到对这些变换的组合。
缩放
我们知道所有的变换都是通过对 Matrix 的操作进行的,并在 onDraw 时将这个 matrix 添加到 canvas 中,这样完成变换。在 Matrix 中对应缩放的 API 有以下 6 个方法:
- Matrix.setScale(float sx, float sy);
- Matrix.preScale(float sx, float sy);
- Matrix.postScale(float sx, float sy);
- Matrix.setScale(float sx, float sy, float px, float py);
- Matrix.preScale(float sx, float sy, float px, float py);
- Matrix.postScale(float sx, float sy, float px, float py);
这几个方法的前两个参数指定了在 X 和 Y 方向上的缩放,即 scaleX 和 scaleY;而四个参数的方法,是可以指定缩放中心的,默认的缩放中心是在原点。这些方法里,带有 pre 和 post 前缀的方法是用于组合变换的,这个我们放到后面介绍变换组合时再讲。这里我们就从 setScale 方法开始介绍变换。
理论上而言,一个点是不存在什么缩放变换的,但考虑到所有图像都是由点组成,因此,如果图像在x轴和y轴方向分别放大k1和k2倍的话,那么图像中的所有点的x坐标和y坐标均会分别放大 k1 和 k2 倍,即
用矩阵表示就是:
基于原点缩放
基于原点的缩放是通过 Matrix.setScale(float sx, float sy) 方法来实现。
下面先通过动画来演示一下 setScale(2F, 0.5F) 的效果:
可见,这个缩放变换在 X轴上扩展到原来2倍而 Y轴缩放到原来的一半。
那么在底层这个 Matrix 又是如何完成变换呢?
我们知道 Matrix 是 3*3 矩阵,上图表示其中各个值的意义。在默认情况下,其值为单位矩阵,即:
csharp
[1.0 0.0 0.0]
[0.0 1.0 0.0]
[0.0 0.0 1.0]
在我们调用 setScale(2F, 0.5F) 之后,矩阵的 SCALE_X 变成了 2,SCALE_Y 变成了 0.5,即:
csharp
[2.0 0.0 0.0]
[0.0 0.5 0.0]
[0.0 0.0 1.0]
我们拿矩形的右下角 (600,600)举例,在应用了这个矩阵后,这个右下角的点就变成了(1200,300):
css
[2.0 0.0 0.0] [600] [1200]
[0.0 0.5 0.0] * [600] = [300 ]
[0.0 0.0 1.0] [ 0 ] [0 ]
基于指定点缩放
上面的函数没有指定缩放点,因此以原点进行缩放。现在我们就看一下这个指定缩放点的方法:Matrix.setScale(float sx, float sy, float px, float py)
我们使用 setScale(2F, 0.5F, 600, 600) 将缩放点设置在图片的右下角,先看一下缩放的效果
可见,图片的右下角由于是缩放点,是不会移动的,长宽分别变为原来的2倍和0.5倍。
看了缩放动图的效果后,我们看一下其底层的变换矩阵是如何构造的。默认的缩放是以原点(0, 0)为中心,如果希望以(px,py)为缩放中心,需要将缩放转换为三步变换操作:
- 平移 (-px, -py),将缩放中心移动到原点
- 缩放 (sx, sy)
- 平移回原位置 (px, py)
对于矩阵运算则是:M=T(px,py)⋅S(sx,sy)⋅T(−px,−py) 其中:
scss
[1, 0, -px]
T(-px, -py) = [0, 1, -py]
[0, 0, 1 ]
[sx, 0, 0]
S(sx, sy) = [ 0, sy, 0]
[ 0, 0, 1]
[1, 0, px]
T(px, py) = [0, 1, py]
[0, 0, 1]
乘法计算如下:
css
[1, 0, px] [sx, 0, 0] [1, 0, -px] [sx, 0, (1-sx)px]
M = [0, 1, py] * [ 0, sy, 0] * [0, 1, -py] = [ 0, sy, (1-sy)py]
[0, 0, 1] [ 0, 0, 1] [0, 0, 1 ] [ 0, 0, 1 ]
根据以上的算法,在我们的例子中,使用的 setScale(2F, 0.5F, 600, 600) 生成的矩阵为:
csharp
[2, 0, -600]
[0, 0.5, 300]
[0, 0, 1 ]
以 (0,0)点为例子,在经过上述的矩阵变换后得到的点为(-600,300):
css
[2, 0, -600] [0] [-600]
[0, 0.5, 300] * [0] = [ 300]
[0, 0, 1 ] [0] [ 0 ]
有些同学可能不了解为什么要这么变换,那么看如下的动图:
通过 GIF 能看到这种指定的缩放中心的方法,其步骤就是三步,也就是上面用于构造矩阵的三步:
- 移动缩放点到原点,对应(600,600)移动到(0,0),因此要平移(-600,-600);
- 进行缩放,这一步简单,就是前一节讲的基于原点缩放;
- 平移回原处,原来缩放点被移动到原点(-600,-600),最后肯定要移动回去,即平移(600,600);
这样通过例子和动图,我想大家应该明白了这种缩放的原理,以及其底层的矩阵。
了解完缩放之后,我们再来看一下旋转。
旋转
同缩放一下,Matrix 中对于旋转的 API,也有如下六个:
- Matrix.setRotate(float degrees);
- Matrix.preRotate(float degrees);
- Matrix.postRotate(float degrees);
- Matrix.setRotate(float degrees, float px, float py);
- Matrix.preRotate(float degrees, float px, float py);
- Matrix.postRotate(float degrees, float px, float py);
带有 pre 和 post 前缀方法是用于变换的组合,我们放到后面再讲。同缩放一样,我们先从 setRotate 开始逐步深入。
围绕原点旋转
假定有一个点 P(x0,y0),相对坐标原点顺时针旋转θ后的情形,同时假定P点离坐标原点的距离为 r,如下图:
那么我们可以表示出这些点的坐标为:
用矩阵表示则为:
有了上面的基础之后,我们再用一个例子来演示旋转,以便大家理解。
基于原点的旋转我们先直接使用 setRotate 方法,这个方法的参数以角度为单位。我们先看下旋转30度的动图再其对应的矩阵是如何生成:
根据上面的矩阵原理,setRotate(30) 方法生成的矩阵为:
css
[cos30°, -sin30°, 0] [0.866, 0.5, 0.0]
[sin30°, cos30°, 0] = [0.5, 0.866, 0.0]
[ 0 , 0 , 1] [0.0, 0.0, 1.0]
当我们以图片的右下角(600,600)为例子,与这个矩阵相乘,就能得到旋转后的新顶点:
css
[0.866, -0.5, 0.0] [600] [219.61523]
[0.5, 0.866, 0.0] * [600] = [819.61523]
[0.0, 0.0, 1.0] [ 1 ] [ 1 ]
可以观察到新顶点为(219,819),这与图片的位置基本对应。这就是基于原点的旋转。
围绕指定点旋转
基于指定点旋转的方法为:Matrix.setRotate(float degrees, float px, float py),这个方法的最后两个参数用于指定旋转中心。而这个方法与基于指定点缩放类似,其在具体实现时也是分为三个步骤:
- 平移(-px,-py),将旋转中心平移到原点
- 旋转 degrees 角度
- 平移回原位置(px, py)
在这里,我们先直接使用 setRotate(float degrees, float px, float py) 看一下旋转的动画和结果,下面是调用 Matrix.setRotate(-30F, 600, 600) 的动画,负的角度表示逆时针旋转:
通过动画能看到图片是绕着右下角(600,600)旋转了 -30°,那么接下来我们在这个动画背后,矩阵做了哪些事情。 前面我们将基于指定点旋转分为了三个步骤,那么就要问了,为什么可以分为这三步呢?这是因为,围绕某个点(Xp,Yp)顺时针旋转θ,那么用矩阵表示就为:
可以转化为:
这就明显看到这一个大矩阵被分为三个矩阵,即 M=T(px,py)⋅R(degrees)⋅T(−px,−py)
在这个例子中,三个矩阵分别为:
scss
[1, 0, -px]
T(-px, -py) = [0, 1, -py]
[0, 0, 1 ]
[cosΘ, -sinΘ, 0]
R(Θ) = [sinΘ, cosΘ, 0]
[ 0, 0, 1]
[1, 0, px]
T(px, py) = [0, 1, py]
[0, 0, 1]
我们套用公式,就可以得到 Matrix.setRotate(-30F, 600, 600) 这个矩阵的值为:
css
[0.866, 0.5, -219.61523]
[ -0.5, 0.866, 380.38477]
[ 0.0, 0.0, 1.0 ]
接下来我们再看下分拆为三个步骤的动图,大家就知道这个值是怎么来的了:
我们可以以图片的左下角(0,600)为例子做一个计算:
css
[0.866, 0.5, -219.61523] [ 0 ] [80.384766]
[ -0.5, 0.866, 380.38477] * [600] = [ 900 ]
[ 0.0, 0.0, 1.0 ] [ 1 ] [ 1 ]
即左下角(0,600)被变换到(80.384766, 900.0),这与图上的表现一致。
平移
平移这个就太简单了,用矩阵表示就是:
对应的 Matrix 方法有 3 个:
- Matrix.setTranslate(float dx, float dy)
- Matrix.preTranslate(float dx, float dy)
- Matrix.postTranslate(float dx, float dy)
平移就没有太多可说的,对应矩阵中的 MTRANS_X 和 MTRANS_Y 值。在界面上的表现只是将点进行简单的平移,这里就不多说了。
后续
后续我们将继续深入到矩阵的变化,聊一聊如果将此文中的几个变换进行组合变换。由于矩阵乘法不满足交换律,因此变换的组合也会稍显复杂。