Android 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变换中有着至关重要的作用,这个目前暂时用不到,所以我也没有深究。

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

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

Matrix基本原理

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

我们所用到的变换均属于仿射变换,仿射变换是 线性变换(缩放,旋转,错切) 和 平移变换(平移) 的复合。

仿射变换概念:仿射变换其实就是二维坐标到二维坐标的线性变换,保持二维图形的“平直性”(即变换后直线还是直线不会打弯,圆弧还是圆弧)和“平行性”(指保持二维图形间的相对位置关系不变,平行线还是平行线,而直线上点的位置顺序不变),可以通过一系列的原子变换的复合来实现,原子变换就包括:平移、缩放、翻转、旋转和错切。这里除了透视可以改变z轴以外,其他的变换基本都是上述的原子变换,所以,只要最后一行是0,0,1则是仿射矩阵。

基本变换有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),由于矩阵乘法不满足交换律,所以前乘(pre),后乘(post)和设置(set)的区别还是很大的。

前乘(pre)

前乘相当于矩阵的右乘:
在这里插入图片描述

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

后乘(post)

前乘相当于矩阵的左乘:
在这里插入图片描述

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

设置(set)

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

组合

首先我们先看一段话,Matrix调用一系列set,pre,post方法时,可视为将这些方法插入到一个队列.当然,按照队列中从头至尾的顺序调用执行,其中pre表示在队头插入一个方法,post表示在队尾插入一个方法.而set表示把当前队列清空,并且总是位于队列的最中间位置.当执行了一次set后:pre方法总是插入到set前部的队列的最前面,post方法总是插入到set后部的队列的最后面

一开始我以为是正确的,后面经过测试验证。这是错误的。答案在错误结论二指出

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

错误结论一: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 先执行,例如:

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

同样化简公式:

// 矩阵乘法满足结合律
M‘ = T*(M*R) = T*M*R = (T*M)*R

从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。

如何理解和使用 pre 和 post ?

不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。

pre  : 右乘, M‘ = M*A
post : 左乘, M’ = A*M

那么如何使用?

正确使用方式就是先构造正常的 Matrix 乘法顺序,之后根据情况使用 pre 和 post 来把这个顺序实现。

还是用一个最简单的例子理解,假设需要围绕某一点旋转。

可以用这个方法 xxxRotate(angle, pivotX, pivotY) ,由于我们这里需要组合构造一个 Matrix,所以不直接使用这个方法。

首先,有两条基本定理:

  • 所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。
  • 之前操作的坐标系状态会保留,并且影响到后续状态。

基于这两条基本定理,我们可以推算出要基于某一个点进行旋转需要如下步骤:

1. 先将坐标系原点移动到指定位置,使用平移 T
2. 对坐标系进行旋转,使用旋转 S (围绕原点旋转)
3. 再将坐标系平移回原来位置,使用平移 -T

具体公式如下:

M 为原始矩阵,是一个单位矩阵, M‘ 为结果矩阵, T 为平移, R为旋转

M' = M*T*R*-T = T*R*-T

按照公式写出来的伪代码如下:

Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
matrix.preRotate(angle);
matrix.preTranslate(-pivotX, -pivotY);

围绕某一点操作可以拓展为通用情况,即:

Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
// 各种操作,旋转,缩放,错切等,可以执行多次。
matrix.preTranslate(-pivotX, -pivotY);

公式为:

M' = M*T* ... *-T = T* ... *-T

但是这种方式,两个调整中心的平移函数就拉的太开了,所以通常采用这种写法:

Matrix matrix = new Matrix();
// 各种操作,旋转,缩放,错切等,可以执行多次。
matrix.postTranslate(pivotX,pivotY);
matrix.preTranslate(-pivotX, -pivotY);

这样公式为:

M' = T*M* ... *-T = T* ... *-T

可以看到最终化简结果是相同的。

所以说,pre 和 post 就是用来调整乘法顺序的,正常情况下应当正向进行构建出乘法顺序公式,之后根据实际情况调整书写即可。

在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。

下面我们用不同对方式来构造一个矩阵:

假设我们需要先缩放再平移。

注意:

  • 1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
  • 2.注意构造顺序,顺序是会影响结果的。
  • 3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
仅用pre:
Matrix m = new Matrix();
m.reset();
m.preTranslate(tx, ty);
m.preScale(sx, sy);

用矩阵表示:
在这里插入图片描述

仅用post:
Matrix m = new Matrix();
m.reset();
m.postScale(sx, sy); 
m.postTranslate(tx, ty);

用矩阵表示:
在这里插入图片描述

混合:
Matrix m = new Matrix();
m.reset();
m.preScale(sx, sy);  
m.postTranslate(tx, ty);

或:

Matrix m = new Matrix();
m.reset();
m.postTranslate(tx, ty);
m.preScale(sx, sy);  

由于此处只有两步操作,且指定了先后,所以代码上交换并不会影响结果。

用矩阵表示:
在这里插入图片描述

注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为空,如果初始矩阵不为空,则导致运算结果不同。

Matrix方法详解

讲解完了matrix的基本原理之后,我们逐个讲解它的方法。

Matrix方法表

方法类别相关API摘要
基本方法equals hashCode toString toShortString比较、 获取哈希值、 转换为字符串
数值操作set reset setValues getValues设置、 重置、 设置数值、 获取数值
数值计算mapPoints mapRadius mapRect mapVectors计算变换后的数值
设置(set)setConcat setRotate setScale setSkew setTranslate设置变换
前乘(pre)preConcat preRotate preScale preSkew preTranslate前乘变换
后乘(post)postConcat postRotate postScale postSkew postTranslate后乘变换
特殊方法setPolyToPoly setRectToRect rectStaysRect setSinCos一些特殊操作
矩阵相关invert isAffine(API21) isIdentity求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 …

Matrix方法详解

构造方法

构造方法没有在上面表格中列出。

无参构造
Matrix ()

创建一个全新的Matrix,使用格式如下:

Matrix matrix = new Matrix();

通过这种方式创建出来的并不是一个数值全部为空的矩阵,而是一个单位矩阵,如下:
在这里插入图片描述

有参构造
Matrix (Matrix src)

这种方法则需要一个已经存在的矩阵作为参数,使用格式如下:

Matrix matrix = new Matrix(src);

创建一个Matrix,并对src深拷贝(理解为新的matrix和src是两个对象,但内部数值相同即可)。

基本方法

基本方法内容比较简单,在此处简要介绍一下。

1.equals

比较两个Matrix的数值是否相同。

2.hashCode

获取Matrix的哈希值。

3.toString

将Matrix转换为字符串: Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}

4.toShortString

将Matrix转换为短字符串: [1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]

数值操作

数值操作这一组方法可以帮助我们直接控制Matrix里面的数值。

1.set
void set (Matrix src)

没有返回值,有一个参数,作用是将参数Matrix的数值复制到当前Matrix中。如果参数为空,则重置当前Matrix,相当于reset()

2.reset
void reset ()

重置当前Matrix(将当前Matrix重置为单位矩阵)。

3.setValues
void setValues (float[] values)

setValues的参数是浮点型的一维数组,长度需要大于9,拷贝数组中的前9位数值赋值给当前Matrix。

4.getValues
void getValues (float[] values)

很显然,getValues和setValues是一对方法,参数也是浮点型的一维数组,长度需要大于9,将Matrix中的数值拷贝进参数的前9位中。

矩阵相关

矩阵相关的函数就属于哪一种非常靠近底层的东西了,大部分开发者很少直接接触这些东西,想要弄明白这个可以回去请教你们的线性代数老师,这里也仅作概述。

方法摘要
invert求矩阵的逆矩阵
isAffine判断当前矩阵是否为仿射矩阵,API21(5.0)才添加的方法。
isIdentity判断当前矩阵是否为单位矩阵。
1.invert

求矩阵的逆矩阵,简而言之就是计算与之前相反的矩阵,如果之前是平移200px,则求的矩阵为反向平移200px,如果之前是缩小到0.5f,则结果是放大到2倍。

boolean invert (Matrix inverse)

简单测试:

Matrix matrix = new Matrix();
Matrix invert = new Matrix();
matrix.setTranslate(200,500);

Log.e(TAG, "before - matrix "+matrix.toShortString() );

Boolean result = matrix.invert(invert);

Log.e(TAG, "after  - result "+result );
Log.e(TAG, "after  - matrix "+matrix.toShortString() );
Log.e(TAG, "after  - invert "+invert.toShortString() );

结果:

before - matrix [1.0, 0.0, 200.0][0.0, 1.0, 500.0][0.0, 0.0, 1.0]
after  - result true
after  - matrix [1.0, 0.0, 200.0][0.0, 1.0, 500.0][0.0, 0.0, 1.0]
after  - invert [1.0, 0.0, -200.0][0.0, 1.0, -500.0][0.0, 0.0, 1.0]
2.isAffine

判断矩阵是否是仿射矩阵, 基本上这个一直是true,因为我们所使用的变换基本上都是放射变换。
仿射矩阵具体查看前面介绍,Matrix原理
简单测试:

Matrix matrix = new Matrix();
Log.i(TAG,"isAffine="+matrix.isAffine());

matrix.postTranslate(200,0);
matrix.postScale(0.5f, 1);
matrix.postSkew(0,1);
matrix.postRotate(56);

Log.i(TAG,"isAffine= " + matrix.isAffine());

结果:

isAffine=true
isAffine=true
3.isIdentity

判断是否为单位矩阵,什么是单位矩阵呢,如图所示:
在这里插入图片描述

新创建的Matrix和重置后的Matrix都是单位矩阵,不过,只要随意操作一步,就不在是单位矩阵了。

简单测试:

Matrix matrix = new Matrix();
Log.i(TAG,"isIdentity= " + matrix.isIdentity());

matrix.postTranslate(200,0);

Log.i(TAG,"isIdentity= " + matrix.isIdentity());

结果:

isIdentity=true
isIdentity=false
4.setConcat
boolean setConcat(Matrix a,Matrix b)
boolean postConcat(Matrix other) 
boolean preConcat(Matrix other)

Matrix类还提供了直接矩阵计算方式。Matrix a=new Matrix()相当于创建一个单位矩阵。

  • a.set(b),就是赋值a = b;
  • a.preConCat(b),相当于前乘,即 a=a×b;
  • a.postConCat(b),相当于前乘,即 a=b×a;
  • c.setConcat(a,b),相当于c=a×b;

对于例子,我们可以设置3个矩阵,分别表示3种图形效果;

Matrix m1 = new Matrix(); 
m1.setScale(interpolatedTime, interpolatedTime);  

Matrix m2 = new Matrix(); 
m2.setTranslate(-centerX, -centerY);  

Matrix m3 = new Matrix(); 
m3.setTranslate(centerX, centerY); 

要实现同样的例子效果,可以:

matrix.set(m1);               //matrix = m1 
matrix.preConcat(m2);   //matrix = matrix * m2 = m1 * m2;
matrix.postConcat(m3); //matrix = m3 * matrix = m 3 * (m1* m2) = m3 * m1 *m2;

或者

matrix.setConcat(m1, m2);      // matrix = m1 * m2; 
matrix.setConcat(m3, matrix); //matrix = m3 * matrix = m3* (m1* m2) = m3 * m1 * m2;

基础操作

1.setTranslate
void setTranslate(float dx, float dy)

设置平移效果,参数分别是x,y上的平移量。

2.setScale
void setScale(float sx, float sy, float px, float py)
void setScale(float sx, float sy)

两个方法都是设置缩放到matrix中,sx,sy代表了缩放的倍数,px,py代表缩放的中心。

3.setRotate
void setRotate(float degrees, float px, float py)
void setRotate(float degrees)

和上面类似,不再讲解。px,py代表旋转的中心。

4.setSkew
void setSkew(float kx, float ky, float px, float py)
void setSkew(float kx, float ky)

错切,这里kx,ky分别代表了x,y上的错切因子,px,py代表了错切的中心。

数值计算

1.mapPoints
void mapPoints (float[] pts)

void mapPoints (float[] dst, float[] src)

void mapPoints (float[] dst, int dstIndex,float[] src, int srcIndex, int pointCount)

计算一组点基于当前Matrix变换后的位置,(由于是计算点,所以参数中的float数组长度一般都是偶数的,若为奇数,则最后一个数值不参与计算)。

它有三个重载方法:

(1) void mapPoints (float[] pts) 方法仅有一个参数,pts数组作为参数传递原始数值,计算结果仍存放在pts中。

示例:

// 初始数据为三个点 (0, 0) (80, 100) (400, 300) 
float[] pts = new float[]{0, 0, 80, 100, 400, 300};

// 构造一个matrix,x坐标缩放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 输出pts计算之前数据
Log.i(TAG, "before: "+ Arrays.toString(pts));

// 调用map方法计算
matrix.mapPoints(pts);

// 输出pts计算之后数据
Log.i(TAG, "after : "+ Arrays.toString(pts));

结果:

before: [0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : [0.0, 0.0, 40.0, 100.0, 200.0, 300.0]

(2) void mapPoints (float[] dst, float[] src) ,src作为参数传递原始数值,计算结果存放在dst中,src不变。

如果原始数据需要保留则一般使用这种方法。

示例:

// 初始数据为三个点 (0, 0) (80, 100) (400, 300)
float[] src = new float[]{0, 0, 80, 100, 400, 300};
float[] dst = new float[6];

// 构造一个matrix,x坐标缩放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 输出计算之前数据
Log.i(TAG, "before: src="+ Arrays.toString(src));
Log.i(TAG, "before: dst="+ Arrays.toString(dst));

// 调用map方法计算
matrix.mapPoints(dst,src);

// 输出计算之后数据
Log.i(TAG, "after : src="+ Arrays.toString(src));
Log.i(TAG, "after : dst="+ Arrays.toString(dst));

结果:

before: src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
before: dst=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
after : src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : dst=[0.0, 0.0, 40.0, 100.0, 200.0, 300.0]

(3) void mapPoints (float[] dst, int dstIndex,float[] src, int srcIndex, int pointCount) 可以指定只计算一部分数值。

参数摘要
dst目标数据(指定写入的数组,变换后的数据)
dstIndex目标数据存储位置起始下标(写入的起始索引,x,y两个坐标算作一对,索引的单位是对,也就是经过两个值才加1 )
src源数据(指定要计算的点)
srcIndex源数据存储位置起始下标
pointCount需要计算的点的个数,每个点有两个值,x和y

示例:

将第二、三个点计算后存储进dst最开始位置。

// 初始数据为三个点 (0, 0) (80, 100) (400, 300)
float[] src = new float[]{0, 0, 80, 100, 400, 300};
float[] dst = new float[6];

// 构造一个matrix,x坐标缩放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 输出计算之前数据
Log.i(TAG, "before: src="+ Arrays.toString(src));
Log.i(TAG, "before: dst="+ Arrays.toString(dst));

// 调用map方法计算(最后一个2表示两个点,即四个数值,并非两个数值)
matrix.mapPoints(dst, 0, src, 2, 2);

// 输出计算之后数据
Log.i(TAG, "after : src="+ Arrays.toString(src));
Log.i(TAG, "after : dst="+ Arrays.toString(dst));

结果:

before: src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
before: dst=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
after : src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : dst=[40.0, 100.0, 200.0, 300.0, 0.0, 0.0]
2.mapRadius
float mapRadius (float radius)

测量半径,由于圆可能会因为画布变换变成椭圆,所以此处测量的是平均半径。

示例:

float radius = 100;
float result = 0;

// 构造一个matrix,x坐标缩放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

Log.i(TAG, "mapRadius: "+radius);

result = matrix.mapRadius(radius);

Log.i(TAG, "mapRadius: "+result);

结果:

mapRadius: 100.0
mapRadius: 70.71068
3.mapRect
boolean mapRect (RectF rect)

boolean mapRect (RectF dst, RectF src)

测量矩形变换后位置。

(1) boolean mapRect (RectF rect) 测量rect并将测量结果放入rect中,返回值是判断矩形经过变换后是否仍为矩形。

示例:

RectF rect = new RectF(400, 400, 1000, 800);

// 构造一个matrix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);
matrix.postSkew(1,0);

Log.i(TAG, "mapRadius: "+rect.toString());

boolean result = matrix.mapRect(rect);

Log.i(TAG, "mapRadius: "+rect.toString());
Log.e(TAG, "isRect: "+ result);

结果:

mapRadius: RectF(400.0, 400.0, 1000.0, 800.0)
mapRadius: RectF(600.0, 400.0, 1300.0, 800.0)
isRect: false

由于使用了错切,所以返回结果为false。

(2) boolean mapRect (RectF dst, RectF src) 测量src并将测量结果放入dst中,返回值是判断矩形经过变换后是否仍为矩形,和之前没有什么太大区别,此处就不啰嗦了。

4.mapVectors

测量向量。

void mapVectors (float[] vecs)

void mapVectors (float[] dst, float[] src)

void mapVectors (float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount)
```setRectToRectsetRectToRect

**mapVectors****mapPoints**基本上是相同的,可以直接参照上面的**mapPoints**使用方法。

而两者唯一的区别就是 **mapVectors** 不会受到位移的影响,由于向量的平移前后是相等的,这符合向量的定律,所以这个方法不会对translate相关的方法产生反应,如果只是调用了translate相关的方法,那么得到的值和原本的一致。如果你不了解的话,请找到以前教过你的老师然后把学费要回来。

区别:

```java
float[] src = new float[]{1000, 800};
float[] dst = new float[2];

// 构造一个matrix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);
matrix.postTranslate(100,100);

// 计算向量, 不受位移影响
matrix.mapVectors(dst, src);
Log.i(TAG, "mapVectors: "+Arrays.toString(dst));

// 计算点
matrix.mapPoints(dst, src);
Log.i(TAG, "mapPoints: "+Arrays.toString(dst));

结果:

mapVectors: [500.0, 800.0]
mapPoints: [600.0, 900.0]

特殊方法

这一类方法看似不起眼,但拿来稍微加工一下就可能制作意想不到的效果。

1.setPolyToPoly
boolean setPolyToPoly (
        float[] src, 	// 原始数组 src [x,y],存储内容为一组点
        int srcIndex, 	// 原始数组开始位置
        float[] dst, 	// 目标数组 dst [x,y],存储内容为一组点
        int dstIndex, 	// 目标数组开始位置
        int pointCount)	// 测控点的数量 取值范围是: 0到4

src 和 dst 是源点集合目标点集;srcIndex 和 dstIndex 是第一个点的偏移;pointCount 是采集的点的个数(个数不能大于 4,因为大于 4 个点就无法计算变换了)
Poly全称是Polygon,多边形的意思,了解了意思大致就能知道这个方法是做什么用的了,应该与PS中自由变换中的扭曲有点类似。

在这里插入图片描述

从参数我们可以了解到setPolyToPoly最多可以支持4个点,这四个点通常为图形的四个角,可以通过这四个角将视图从矩形变换成其他形状。

我们知道pointCount支持点的个数为0到4个,四个一般指图形的四个角,属于最常用的一种情形,但前面几种是什么情况呢?

最后一个参数是0到3的话,这几种情况在实际开发中很少会出现。

pointCount摘要
0相当于reset
1相当于translate
2可以进行 缩放、旋转、平移 变换
3可以进行 缩放、旋转、平移、错切 变换
4可以进行 缩放、旋转、平移、错切以及任何形变

从上表我们可以观察出一个规律, 随着pointCount数值增大setPolyToPoly的可以操作性也越来越强,这不是废话么,可调整点数多了能干的事情自然也多了。
只列一个表格就算交代完毕了显得诚意不足,为了彰显诚意,接下来详细的讲解一下。

为什么说前面几种情况在实际开发中很少出现?

作为开发人员,写出来的代码出了要让机器"看懂",没有歧义之外,最重要的还是让人看懂,以方便后期的维护修改,从上边的表格中可以看出,前面的几种种情况都可以有更直观的替代方法,只有四个参数的情况下的特殊形变是没有替代方法的。

测控点选取位置?

测控点可以选择任何你认为方便的位置,只要src与dst一一对应即可。不过为了方便,通常会选择一些特殊的点: 图形的四个角,边线的中心点以及图形的中心点等。不过有一点需要注意,测控点选取都应当是不重复的(src与dst均是如此),如果选取了重复的点会直接导致测量失效,这也意味着,你不允许将一个方形(四个点)映射为三角形(四个点,但其中两个位置重叠),但可以接近于三角形。

作用范围?

作用范围当然是设置了Matrix的全部区域,如果你将这个Matrix赋值给了Canvas,它的作用范围就是整个画布,如果你赋值给了Bitmap,它的作用范围就是整张图片。


接下来用示例演示一下,所有示例的src均为图片大小,dst根据手势变化。

pointCount为0

pointCount为0和reset是等价的,而不是保持matrix不变,在最底层的实现中可以看到这样的代码:

if (0 == count) {
    this->reset();
    return true;
}

在这里插入图片描述
pointCount为1

pointCount为0和translate是等价的,在最底层的实现中可以看到这样的代码:

if (1 == count) {
    this->setTranslate(dst[0].fX - src[0].fX, dst[0].fY - src[0].fY);
    return true;
}

平移的距离是dst - src.

当测控点为1的时候,由于你只有一个点可以控制,所以你只能拖拽着它在2D平面上滑动。
在这里插入图片描述
pointCount为2

当pointCount为2的时候,可以做缩放、平移和旋转。
在这里插入图片描述
pointCount为3

当pointCount为3的时候,可以做缩放、平移、旋转和错切。

在这里插入图片描述
pointCount为4

当pointCount为4的时候,你可以将图像拉伸为任意四边形。
在这里插入图片描述
上面已经用图例比较详细的展示了不同操控点个数的情况,如果你依旧存在疑问,可以查看Demo,实际操作一遍。

2.setRectToRect
boolean setRectToRect (RectF src, 			// 源区域
                RectF dst, 					// 目标区域
                Matrix.ScaleToFit stf)		// 缩放适配模式

简单来说就是将源矩形的内容填充到目标矩形中,然而在大多数的情况下,源矩形和目标矩形的长宽比是不一致的,到底该如何填充呢,这个填充的模式就由第三个参数 stf 来确定。

ScaleToFit 是一个枚举类型,共包含了四种模式:

模式摘要
CENTER居中,对src等比例缩放,并最大限度的填充变换后的矩形,将其居中放置在dst中。
START顶部,对src等比例缩放,并最大限度的填充变换后的矩形,将其放置在dst的左上角。左上对齐
END底部,对src等比例缩放,并最大限度的填充变换后的矩形,将其放置在dst的右下角。 右下对齐
FILL充满,拉伸src的宽和高,使其完全填充满dst。

下面我们看一下不同宽高比的src与dst在不同模式下是怎样的。
最上层的是src 底下部分是变换后的dst

这里使用谷歌的api demo的图片作为例子:

具体事例,请看Demo

3.rectStaysRect
boolean rectStaysRect()

判断矩形经过变换后是否仍为矩形,假如Matrix进行了平移、缩放则画布仅仅是位置和大小改变,矩形变换后仍然为矩形,但Matrix进行了非90度倍数的旋转或者错切,则矩形变换后就不再是矩形了,这个很好理解,不过多赘述,顺便说一下,前面的mapRect方法的返回值就是根据rectStaysRect来判断的。

4.setSinCos

// 方法一
void setSinCos (float sinValue, 	// 旋转角度的sin值
                float cosValue)		// 旋转角度的cos值

// 方法二
void setSinCos (float sinValue, 	// 旋转角度的sin值
                float cosValue, 	// 旋转角度的cos值
                float px, 			// 中心位置x坐标
                float py)			// 中心位置y坐标

设置sinCos值,这个是控制Matrix旋转的,由于Matrix已经封装好了Rotate方法,所以这个并不常用,在此仅作概述。
在这里插入图片描述
其实旋转,就是使用了这样的matrix,显而易见,这里的参数就清晰了。
sinValue:对应图中的sin值
cosValue:对应cos值
px:中心的x坐标
py:中心的y坐标

看一个示例,我们把图像旋转90度,那么90度对应的sin90=1和cos90=0

Matrix matrix = new Matrix();
matrix.setSinCos(1f, 0f);
Log.i(TAG, "setSinCos: " + matrix.toShortString());

// 重置
matrix.reset();
// 旋转90度
matrix.setRotate(90);
Log.i(TAG, "setRotate: " + matrix.toShortString());

结果:

setSinCos:[0.0, -1.0, 0.0][1.0, 0.0, 0.0][0.0, 0.0, 1.0]
setRotate:[0.0, -1.0, 0.0][1.0, 0.0, 0.0][0.0, 0.0, 1.0]

Matrix实用技巧

通过前面的代码和示例,我们已经了解了Matrix大部分方法是如何使用的,这些基本的原理和方法通过组合可能会创造出神奇的东西,网上有很多教程讲Bitmap利用Matrix变换来制作镜像倒影等,这都属于Matrix的基本应用,我就不在赘述了,下面我简要介绍几种然并卵的小技巧,更多的大家可以开启自己的脑洞来发挥。

1.获取View在屏幕上的绝对位置

开头我们提到过Matrix最根本的作用就是坐标映射,将View的相对坐标映射为屏幕的绝对坐标,也提到过我们在onDraw函数的canvas中获取到到Matrix并不是单位矩阵,结合这两点,聪明的你肯定想到了我们可以从canvas的Matrix入手取得View在屏幕上的绝对位置。

不过,这也仅仅是一个然并卵的小技巧而已,使用getLocationOnScreen同样可以获取View在屏幕的位置

简单示例:

@Override
protected void onDraw(Canvas canvas) {
    float[] values = new float[9];
    int[] location1 = new int[2];

    Matrix matrix = canvas.getMatrix();
    matrix.getValues(values);

    location1[0] = (int) values[2];
    location1[1] = (int) values[5];
    Log.i(TAG, "location1 = " + Arrays.toString(location1));

    int[] location2 = new int[2];
    this.getLocationOnScreen(location2);
    Log.i(TAG, "location2 = " + Arrays.toString(location2));
}

结果:

location1 = [0, 243]
location2 = [0, 243]

2.利用setPolyToPoly制造3D效果

鸿洋大神发过一篇博文详细讲解了利用setPolyToPoly制造的折叠效果布局,大家直接到他的博客去看吧。

图片引用自鸿洋大大的博客,稍作了一下处理。

在这里插入图片描述

博文链接:

Android FoldingLayout 折叠布局 原理及实现(一)

Android FoldingLayout 折叠布局 原理及实现(二)

项目Demo 点击下载

本文参考了作者GcsSloop https://github.com/GcsSloop/AndroidNote/tree/master/CustomView
在他的基础上做了点修改。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值