安卓自定义 View 进阶:Matrix 原理

本文内容偏向理论,和画布操作有重叠的部分,本文会让你更加深入的了解其中的原理。

本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们所有看到View背后都有着Matrix的功劳,但我们却很少见到它,本篇我们就看看它是何方神圣吧。

由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。

⚠️ 警告:测试本文章示例之前请关闭硬件加速。

Matrix简介

Matrix是一个矩阵,主要功能是坐标映射,数值转换。

它看起来大概是下面这样:

Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:

我的的手机屏幕作为物理设备,其物理坐标系是从左上角开始的,但我们在开发的时候通常不会使用这一坐标系,而是使用内容区的坐标系。

以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。

假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。

以上是仅作为一个简单的示例,实际上不论2D还是3D,我们要将图形显示在屏幕上,都离不开Matrix,所以说Matrix是一个在背后辛勤工作的劳模。

Matrix特点

  • 作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
  • 更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
  • 封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
  • 难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。

常见误解

1.认为Matrix最下面的一行的三个参数(MPERSP_0、MPERSP_1、MPERSP_2)没有什么太大的作用,在这里只是为了凑数。

实际上最后一行参数在3D变换中有着至关重要的作用,这一点会在后面中Camera一文中详细介绍。

2.最后一个参数MPERSP_2被解释为scale

的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。

Matrix基本原理

Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:

我们所用到的变换均属于仿射变换,仿射变换是 线性变换(缩放,旋转,错切) 和 平移变换(平移) 的复合,由于这些概念对于我们作用并不大,此处不过多介绍,有兴趣可自行了解。

基本变换有4种: 平移(translate)、缩放(scale)、旋转(rotate) 和 错切(skew)。

下面我们看一下四种变换都是由哪些参数控制的。

从上图可以看到最后三个参数是控制透视的,这三个参数主要在3D效果中运用,通常为(0, 0, 1),不在本篇讨论范围内,暂不过多叙述,会在之后对文章中详述其作用。

由于我们以下大部分的计算都是基于矩阵乘法规则,如果你已经把线性代数还给了老师,请参考一下这里:
维基百科-矩阵乘法

1.缩放(Scale)

用矩阵表示:

你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
(x, y, 1) - 点;
(x, y, 0) - 向量;
另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)...(2N,3N,N)表示的均是(2,3)这一个点。(将MPERSP_2解释为scale这一误解就源于此)。

图例:

2.错切(Skew)

错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。

水平错切

用矩阵表示:

图例:

垂直错切

用矩阵表示:

图例:

复合错切

水平错切和垂直错切的复合。

用矩阵表示:

图例:

3.旋转(Rotate)

假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:

用矩阵表示:

图例:

4.平移(Translate)

此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。

用矩阵表示:

图例:

Matrix复合原理

其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。

我们常用的四大变换操作,每一种操作在Matrix均有三类,前乘(pre),后乘(post)和设置(set),可以参见文末对Matrix方法表,由于矩阵乘法不满足交换律,所以前乘(pre),后乘(post)和设置(set)的区别还是很大的。

前乘(pre)

前乘相当于矩阵的右乘:

这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。

后乘(post)

前乘相当于矩阵的左乘:

这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。

设置(set)

设置使用的不是矩阵乘法,而是直接覆盖掉原来的数值,所以,使用设置可能会导致之前的操作失效

组合

关于 Matrix 的文章终有一个问题,就是 pre 和 post 这一部分的理论非常别扭,国内大多数文章都是这样的,看起来貌似是对的但很难理解,部分内容违背直觉。

我由于也受到了这些文章的影响,自然而然的继承了这一理论,直到在评论区有一位小伙伴提出了一个问题,才让我重新审视了这一部分的内容,并进行了一定反思。

经过良久的思考之后,我决定抛弃国内大部分文章的那套理论和结论,只用严谨的数学逻辑和程序逻辑来阐述这一部分的理论,也许仍有疏漏,如有发现请指正。

首先澄清两个错误结论,记住,是错误结论,错误结论,错误结论。

错误结论一:pre 是顺序执行,post 是逆序执行。

这个结论很具有迷惑性,因为这个结论并非是完全错误的,你很容易就能证明这个结论,例如下面这样:

  • // 第一段 pre 顺序执行,先平移(T)后旋转(R)
  • Matrix matrix = new Matrix();
  • matrix.preTranslate(pivotX,pivotY);
  • matrix.preRotate(angle);
  • Log.e("Matrix", matrix.toShortString());
  • // 第二段 post 逆序执行,先平移(T)后旋转(R)
  • Matrix matrix = new Matrix();
  • matrix.postRotate(angle);
  • matrix.postTranslate(pivotX,pivotY)
  • Log.e("Matrix", matrix.toShortString());

这两段代码最终结果是等价的,于是轻松证得这个结论的正确性,但事实真是这样么?

首先,从数学角度分析,pre 和 post 就是右乘或者左乘的区别,其次,它们不可能实际影响运算顺序(程序执行顺序)。以上这两段代码等价也仅仅是因为最终化简公式一样而已。

设原始矩阵为 M,平移为 T ,旋转为 R ,单位矩阵为 I ,最终结果为 M'

  • 矩阵乘法不满足交换律,即 A\\B ≠ B\\A
  • 矩阵乘法满足结合律,即 (A\\B)\\C = A\\(B\\C)
  • 矩阵与单位矩阵相乘结果不变,即 A * I = A
  • 由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得:
  • // 第一段 pre
  • M' = (M*T)*R = I*T*R = T*R
  • // 第二段 post
  • M' = T*(R*M) = T*R*I = T*R

由于两者最终的化简公式是相同的,所以两者是等价的,但是,这结论不具备普适性。

即原始矩阵不为单位矩阵的时候,两者无法化简为相同的公式,结果自然也会不同。另外,执行顺序就是程序书写顺序,不存在所谓的正序逆序。

错误结论二:pre 是先执行,而 post 是后执行。

这一条结论比上一条更离谱。

之所以产生这个错误完全是因为写文章的人懂英语。

  • pre :先,和 before 相似。
  • post :后,和 after 相似。

所以就得出了 pre 先执行,而 post 后执行这一说法,但从严谨的数学和程序角度来分析,完全是不可能的,还是上面所说的,pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行,属于完全扯淡型。

如果非要用这套理论强行解释的话,反而看起来像是 post 先执行,例如:

```java
matrix.preRotate(angle);
matrix.postTranslate(pivotX,pivotY);

top Created with Sketch.