深入理解 Android Canvas 变换:缩放、旋转、平移全解析(一)

在 Android 开发中,Canvas 是自定义 View 绘制的核心工具,而 变换(Transformations) 让我们可以灵活地操控图像,创造丰富的视觉效果。这些变换包括 旋转(rotate)、缩放(scale) 还是 平移(translate),合理地使用这些变换不仅可以优化绘制流程,还能减少资源消耗。

在本文中,我们将深入探讨 Canvas 变换的工作原理,分析 Matrix 矩阵的底层计算逻辑,并通过实际案例用动画帮助你掌握 Canvas 变换。

缩放、旋转、平移的单个变换

既然要讲解 Canvas 变换,我们就先从最简单的单个变换开始,然后逐步深入对单个变换进行各种组合。

在这篇文章中,我将以下面的这个600*600 的正方形图片为例进行变换,通过这个正方形图片能够很明显地展示出变换是如何进行的:

这里注意背景的网格线,都是以 200 为间隔,通过这些网格线能更方便地进行观察。另外,红色的线则是X轴和Y轴,并且将原点从左上角移动到了两条红色线交点的位置。

下面我们就从单个变换开始,再逐步深入到对这些变换的组合。

缩放

我们知道所有的变换都是通过对 Matrix 的操作进行的,并在 onDraw 时将这个 matrix 添加到 canvas 中,这样完成变换。在 Matrix 中对应缩放的 API 有以下 6 个方法:

  1. Matrix.setScale(float sx, float sy);
  2. Matrix.preScale(float sx, float sy);
  3. Matrix.postScale(float sx, float sy);
  4. Matrix.setScale(float sx, float sy, float px, float py);
  5. Matrix.preScale(float sx, float sy, float px, float py);
  6. 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)为缩放中心,需要将缩放转换为三步变换操作:

  1. 平移 (-px, -py),将缩放中心移动到原点
  2. 缩放 (sx, sy)
  3. 平移回原位置 (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 能看到这种指定的缩放中心的方法,其步骤就是三步,也就是上面用于构造矩阵的三步:

  1. 移动缩放点到原点,对应(600,600)移动到(0,0),因此要平移(-600,-600);
  2. 进行缩放,这一步简单,就是前一节讲的基于原点缩放;
  3. 平移回原处,原来缩放点被移动到原点(-600,-600),最后肯定要移动回去,即平移(600,600);

这样通过例子和动图,我想大家应该明白了这种缩放的原理,以及其底层的矩阵。

了解完缩放之后,我们再来看一下旋转。

旋转

同缩放一下,Matrix 中对于旋转的 API,也有如下六个:

  1. Matrix.setRotate(float degrees);
  2. Matrix.preRotate(float degrees);
  3. Matrix.postRotate(float degrees);
  4. Matrix.setRotate(float degrees, float px, float py);
  5. Matrix.preRotate(float degrees, float px, float py);
  6. 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),这个方法的最后两个参数用于指定旋转中心。而这个方法与基于指定点缩放类似,其在具体实现时也是分为三个步骤:

  1. 平移(-px,-py),将旋转中心平移到原点
  2. 旋转 degrees 角度
  3. 平移回原位置(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 个:

  1. Matrix.setTranslate(float dx, float dy)
  2. Matrix.preTranslate(float dx, float dy)
  3. Matrix.postTranslate(float dx, float dy)

平移就没有太多可说的,对应矩阵中的 MTRANS_X 和 MTRANS_Y 值。在界面上的表现只是将点进行简单的平移,这里就不多说了。

后续

后续我们将继续深入到矩阵的变化,聊一聊如果将此文中的几个变换进行组合变换。由于矩阵乘法不满足交换律,因此变换的组合也会稍显复杂。

相关推荐
MyhEhud21 分钟前
Kotlin apply 方法的用法和使用场景
android·kotlin·kotlin apply函数
Code额22 分钟前
MySQL的事务机制
android·mysql·adb
蓝莓浆糊饼干2 小时前
请简述一下String、StringBuffer和“equals”与“==”、“hashCode”的区别和使用场景
android·java
_一条咸鱼_4 小时前
Android Retrofit 框架日志与错误处理模块深度剖析(七)
android
顾林海4 小时前
Flutter Dart 面向对象编程全面解析
android·前端·flutter
去伪存真4 小时前
摸着石头过河,重新支棱起Capacitor老项目
android·前端
二流小码农5 小时前
鸿蒙开发:权限管理之权限声明
android·ios·harmonyos
tangweiguo030519875 小时前
Android,Java,Kotlin 确保线程顺序执行的多种实现方式
android·java·kotlin
每次的天空6 小时前
kotlin与MVVM结合使用总结(一)
android·开发语言·kotlin