写在前面:
本文主要通过学习安卓自定义View进阶-Matrix原理这篇文章去学习Matrix 的原理,因此大部分文字出自这篇文章,中间穿插了部分文字是学习中遇到的问题以及解释,并且稍微改变了一下排版,利于本人理解。
首先来看Matrix的定义。
Matrix
官方是这样描述的:
The Matrix class holds a 3x3 matrix for transforming coordinates.
机翻后意思是 Matrix类 拥有一个用于 转换坐标 的 3x3 矩阵。
重点:
1. 3x3 矩阵
2.作用是 转换坐标
1.先来看3x3 矩阵。
官方文档中定义Constants如下。
MPERSP_0,Constant Value: 6
MPERSP_1,Constant Value: 7
MPERSP_2,Constant Value: 8
MSCALE_X,Constant Value: 0
MSCALE_Y,Constant Value: 4
MSKEW_X,Constant Value: 1
MSKEW_Y,Constant Value: 3
MTRANS_X,Constant Value: 2
MTRANS_Y,Constant Value: 5
上面的定义可能不是很好理解,下面的图其实更简单明了。3x3矩阵定义如下所示:
2.转换坐标
Matrix功能是 转换坐标 ,那么为什么需要转换坐标 呢? 举一个简单的例子:
以下文字出处:https://www.gcssloop.com/customview/Matrix_Basic
我的的手机屏幕作为物理设备,其物理坐标系是从左上角开始的,但我们在开发的时候通常不会使用这一坐标系,而是使用内容区的坐标系。
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
以上是仅作为一个简单的示例,实际上不论2D还是3D,我们要将图形显示在屏幕上,都离不开Matrix,所以说Matrix是一个在背后辛勤工作的劳模。
Matrix特点:
1.作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
2.更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
3.封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
4.难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
简单了解了Matrix的定义后,需要看Matrix变换坐标的原理了。但是,在看原理之前,首先要了解Matrix变换坐标的四种基本类型。
以下文字出处:https://www.gcssloop.com/customview/Matrix_Basic
首先,我们所用到的变换 属于 仿射变换,仿射变换是 线性变换(缩放,旋转,错切) 和 平移变换(平移) 的复合。
这里 Matrix基本变换有 4种 :
1.缩放(scale)
2.错切(skew)
3.旋转(rotate)
4.平移(translate)
下面我们看一下四种变换都是由哪些参数控制的。
下面的知识点需要用到矩阵乘法,可以参考这里,简单复习一下矩阵乘法。
参考链接:
https://www.cnblogs.com/alantu2018/p/8528299.html
https://baike.baidu.com/item/%E7%9F%A9%E9%98%B5%E4%B9%98%E6%B3%95/5446029?fr=aladdin
以下大部分文字出自这篇文章:https://www.gcssloop.com/customview/Matrix_Basic
下面来看使用Matrix变换的原理。
1.缩放(Scale)
首先,给出一个简单的缩放例子。坐标(200,200)的点,缩放到(100,70)。
用线性方程式表示缩放的话,如下所示:
用矩阵表示:
你可能注意到了,我们坐标多了一个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轴)。
首先看,什么叫错切。
在某方向上,按照一定的比例对图形的每个点到某条平行于该方向的直线的有向距离做放缩得到的平面图形。
参考链接:
https://baike.baidu.com/item/%E9%94%99%E5%88%87/7024094?fr=aladdin
这句话其实是很难看懂的,具体的图形结合公式更容易理解。
2.1 水平错切
用线性方程式表示:
用矩阵表示:
水平错切综合来看,图形由正方形变成了四边形,但是图形的y轴坐标点没有发生变化,简单看四个顶点的y坐标始终是跟之前一致,发生变化的只有x轴坐标。扩大范围去看整体,所有对应线条的y坐标也是没有变化的。
水平错切:x坐标改变。
2.2 垂直错切
用线性方程式表示:
用矩阵表示:
图例:
垂直错切与水平错切正好相反,图形的x坐标没有发生改变,y坐标发生了变化。
垂直错切:y坐标改变。
2.3 复合错切
水平错切和垂直错切的复合。
用线性方程式表示:
用矩阵表示:
图例:
复合错切:x、y坐标都改变。
一个简单的应用:
Android学习笔记进阶十之Matrix错切变换
3.旋转(Rotate)
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 。
用线性方程式表示:
用矩阵表示:
图例:
旋转相对错切应该算是用的比较多的了,而且也相对容易理解。
4.平移(Translate)
此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
我觉得想出这种做法的人。。数学真好。
用线性方程式表示:
用矩阵表示:
图例:
这里定义了2个点:起点A(x0,y0),终点B(x,y)。
△x = x - x0,△y = y - y0。
其实看到后面,发现这4种变换的定义,都是一个套路。
x = k1 * x0 + xxx
y = k2 * y0 + xxx
到此Matrix 的基本原理已经有了一定的了解。下面来看Matrix 的复合操作。
Matrix 复合操作
首先,要知道什么是Matrix 复合操作。
前面,我们了解到了基本的常用矩阵变换操作有4种:平移、缩放、旋转、斜切。
每种变换都对应一个变换矩阵,通过矩阵乘法,可以把多个变换矩阵相乘得到复合变换矩阵。
简单来说,应该就是2个或2个以上的Matrix基本变换组合而成的操作吧。
针对基本4种操作,每一种操作在Matrix类中均有三类,前乘(pre),后乘(post)和设置(set)这3个函数。
使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
由于函数太多,所以通过
postConcat(Matrix other)
preConcat(Matrix other)
setConcat(Matrix a, Matrix b)
这3个去简单理解pre、post、set。
preConcat (Matrix other)
Preconcats the matrix with the specified matrix. M' = M * other
前乘
postConcat(Matrix other)
Postconcats the matrix with the specified matrix. M' = other * M
后乘
setConcat (Matrix a, Matrix b)
Set the matrix to the concatenation of the two specified matrices and return true.Either of the two matrices may also be the target matrix, that is matrixA.setConcat(matrixA, matrixB); is valid.
设置
设置使用的不是矩阵乘法,而是直接覆盖掉原来的数值。set用于设置单位矩阵中的值。我们通过new Matrix()得到的是一个单位矩阵,后续的矩阵变换都是针对这个单位矩阵进行变换。如Matrix.setRotate(90)、Matrix.setTranslate(10,20)等。
关于pre 和post,以前存在2个错误的理论。
错误结论一:pre 是顺序执行,post 是逆序执行。
错误结论二:pre 是先执行,而 post 是后执行。
现在这个理论应该很少见。其实看函数定义就知道了。总之,记住一点,不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。
使用 pre 和 post
在构造 Matrix 时,尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。
下面我们用不同对方式来构造一个相同的矩阵:
注意:
1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
2.注意构造顺序,顺序是会影响结果的。
3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
其实我觉得,前乘是初始M在前,函数的形参
后乘是初始M在后。
1.仅用pre:
// 使用pre, M' = M*T*S = T*S
Matrix m = new Matrix();
m.reset();
m.preTranslate(tx, ty);
m.preScale(sx, sy);
上面的代码,用矩阵表示:
2.仅用post:
// 使用post, M‘ = T*S*M = T*S
Matrix m = new Matrix();
m.reset();
m.postScale(sx, sy); //,越靠前越先执行。
m.postTranslate(tx, ty);
上面的代码,用矩阵表示:
3.pre和post混合:
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.preScale(sx, sy);
m.postTranslate(tx, ty);
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.postTranslate(tx, ty);
m.preScale(sx, sy);
由于此处只有两步操作,且指定了先后,所以代码上交换并不会影响结果。
用矩阵表示:
前面的部分其实我稍微有一个疑惑,为什么T*M = T
其实是因为还设计到一个知识点:IDENTITY_MATRIX(单位矩阵)
单位矩阵I和矩阵M相乘,得到的结果还是M。所以有 I*M= M*I=M
据这篇文章 Matrix源码解析 所说
Matrix的构造方法:
/**
* Create an identity matrix
*/
public Matrix() {
native_instance = native_create(0);
}
构造出的矩阵长这样:
即一个单位矩阵。
因此,这种情况下pre和post操作的结果是一样的。
需要说明的是,虽然看上去pre和post操作可以写成矩阵链相乘的形式,但是实际上还是按照出现的先后顺序计算的。
Matrix原理部分内容就到这里为止了,下面是api的使用,由于这部分较为容易理解就不再重复,直接看就好。
安卓自定义View进阶-Matrix详解
https://www.gcssloop.com/customview/Matrix_Method