以下转自:
http://www.gcssloop.com/customview/Matrix_Method
http://www.gcssloop.com/customview/Matrix_Basic
这应该是目前最详细的一篇讲解Matrix的中文文章了,在上一篇文章Matrix原理中,我们对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 = new Matrix();
通过这种方式创建出来的并不是一个数值全部为空的矩阵,而是一个单位矩阵,如下:
Matrix (Matrix src)
这种方法则需要一个已经存在的矩阵作为参数,使用格式如下:
Matrix matrix = new Matrix(src);
创建一个Matrix,并对src深拷贝(理解为新的matrix和src是两个对象,但内部数值相同即可)。
基本方法内容比较简单,在此处简要介绍一下。
比较两个Matrix的数值是否相同。
获取Matrix的哈希值。
将Matrix转换为字符串: Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}
将Matrix转换为短字符串: [1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]
数值操作这一组方法可以帮助我们直接控制Matrix里面的数值。
void set (Matrix src)
没有返回值,有一个参数,作用是将参数Matrix的数值复制到当前Matrix中。如果参数为空,则重置当前Matrix,相当于reset()
。
void reset ()
重置当前Matrix(将当前Matrix重置为单位矩阵)。
void setValues (float[] values)
setValues的参数是浮点型的一维数组,长度需要大于9,拷贝数组中的前9位数值赋值给当前Matrix。
void getValues (float[] values)
很显然,getValues和setValues是一对方法,参数也是浮点型的一维数组,长度需要大于9,将Matrix中的数值拷贝进参数的前9位中。
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 | 目标数据存储位置起始下标 |
src | 源数据 |
srcIndex | 源数据存储位置起始下标 |
pointCount | 计算的点个数 |
示例:
将第二、三个点计算后存储进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]
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
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中,返回值是判断矩形经过变换后是否仍为矩形,和之前没有什么太大区别,此处就不啰嗦了。
测量向量。
void mapVectors (float[] vecs)
void mapVectors (float[] dst, float[] src)
void mapVectors (float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount)
mapVectors
与 mapPoints
基本上是相同的,可以直接参照上面的mapPoints
使用方法。
而两者唯一的区别就是mapVectors
不会受到位移的影响,这符合向量的定律,如果你不了解的话,请找到以前教过你的老师然后把学费要回来。
区别:
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]
对于四种基本变换 平移(translate)、缩放(scale)、旋转(rotate)、 错切(skew) 它们每一种都三种操作方法,分别为 设置(set)、 前乘(pre) 和 后乘 (post)。而它们的基础是Concat,通过先构造出特殊矩阵然后用原始矩阵Concat特殊矩阵,达到变换的结果。
关于四种基本变换的知识和三种对应操作的区别,详细可以参考 Canvas之画布操作 和 Matrix原理 这两篇文章的内容。
由于之前的文章已经详细的讲解过了它们的原理与用法,所以此处就简要的介绍一下:
方法 | 简介 |
---|---|
set | 设置,会覆盖掉之前的数值,导致之前的操作失效。 |
pre | 前乘,相当于矩阵的右乘, M' = M * S (S指为特殊矩阵) |
post | 后乘,相当于矩阵的左乘,M' = S * M (S指为特殊矩阵) |
Matrix 相关的重要知识:
1.一开始从Canvas中获取到到Matrix并不是初始矩阵,而是经过偏移后到矩阵,且偏移距离就是距离屏幕左上角的位置。
这个可以用于判定View在屏幕上的绝对位置,View可以根据所处位置做出调整。
2.构造Matrix时使用的是矩阵乘法,前乘(pre)与后乘(post)结果差别很大。
这个直接参见上一篇文章 Matrix原理 即可。
3.受矩阵乘法影响,后面的执行的操作可能会影响到之前的操作。
使用时需要注意构造顺序。
这一类方法看似不起眼,但拿来稍微加工一下就可能制作意想不到的效果。
boolean setPolyToPoly (
float[] src, // 原始数组 src [x,y],存储内容为一组点
int srcIndex, // 原始数组开始位置
float[] dst, // 目标数组 dst [x,y],存储内容为一组点
int dstIndex, // 目标数组开始位置
int pointCount) // 测控点的数量 取值范围是: 0到4
Poly全称是Polygon,多边形的意思,了解了意思大致就能知道这个方法是做什么用的了,应该与PS中自由变换中的扭曲有点类似。
从参数我们可以了解到setPolyToPoly最多可以支持4个点,这四个点通常为图形的四个角,可以通过这四个角将视图从矩形变换成其他形状。
简单示例:
public class MatrixSetPolyToPolyTest extends View {
private Bitmap mBitmap; // 要绘制的图片
private Matrix mPolyMatrix; // 测试setPolyToPoly用的Matrix
public MatrixSetPolyToPolyTest(Context context) {
super(context);
initBitmapAndMatrix();
}
private void initBitmapAndMatrix() {
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.poly_test);
mPolyMatrix = new Matrix();
float[] src = {0, 0, // 左上
mBitmap.getWidth(), 0, // 右上
mBitmap.getWidth(), mBitmap.getHeight(), // 右下
0, mBitmap.getHeight()}; // 左下
float[] dst = {0, 0, // 左上
mBitmap.getWidth(), 400, // 右上
mBitmap.getWidth(), mBitmap.getHeight() - 200, // 右下
0, mBitmap.getHeight()}; // 左下
// 核心要点
mPolyMatrix.setPolyToPoly(src, 0, dst, 0, src.length >> 1); // src.length >> 1 为位移运算 相当于处以2
// 此处为了更好的显示对图片进行了等比缩放和平移(图片本身有点大)
mPolyMatrix.postScale(0.26f, 0.26f);
mPolyMatrix.postTranslate(0,200);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 根据Matrix绘制一个变换后的图片
canvas.drawBitmap(mBitmap, mPolyMatrix, null);
}
}
文章发出后有小伙伴在GitHub上提出疑问,说此处讲解到并不清楚,尤其是最后的一个参数,所以特此补充一下内容。
我们知道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的时候,你可以将图像拉伸为任意四边形。
上面已经用图例比较详细的展示了不同操控点个数的情况,如果你依旧存在疑问,可以获取代码自己试一下。
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在不同模式下是怎样的。
假设灰色部分是dst,橙色部分是src,由于是测试不同宽高比,示例中让dst保持不变,看两种宽高比的src在不同模式下填充的位置。
src(原始状态) | |
---|---|
CENTER | |
START | |
END | |
FILL |
下面用代码演示一下居中的示例:
public class MatrixSetRectToRectTest extends View {
private static final String TAG = "MatrixSetRectToRectTest";
private int mViewWidth, mViewHeight;
private Bitmap mBitmap; // 要绘制的图片
private Matrix mRectMatrix; // 测试etRectToRect用的Matrix
public MatrixSetRectToRectTest(Context context) {
super(context);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rect_test);
mRectMatrix = new Matrix();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF src= new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight() );
RectF dst = new RectF(0, 0, mViewWidth, mViewHeight );
// 核心要点
mRectMatrix.setRectToRect(src,dst, Matrix.ScaleToFit.CENTER);
// 根据Matrix绘制一个变换后的图片
canvas.drawBitmap(mBitmap, mRectMatrix, new Paint());
}
}
判断矩形经过变换后是否仍为矩形,假如Matrix进行了平移、缩放则画布仅仅是位置和大小改变,矩形变换后仍然为矩形,但Matrix进行了非90度倍数的旋转或者错切,则矩形变换后就不再是矩形了,这个很好理解,不过多赘述,顺便说一下,前面的mapRect
方法的返回值就是根据rectStaysRect
来判断的。
设置sinCos值,这个是控制Matrix旋转的,由于Matrix已经封装好了Rotate方法,所以这个并不常用,在此仅作概述。
// 方法一
void setSinCos (float sinValue, // 旋转角度的sin值
float cosValue) // 旋转角度的cos值
// 方法二
void setSinCos (float sinValue, // 旋转角度的sin值
float cosValue, // 旋转角度的cos值
float px, // 中心位置x坐标
float py) // 中心位置y坐标
简单测试:
Matrix matrix = new Matrix();
// 旋转90度
// sin90=1
// cos90=0
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]
矩阵相关的函数就属于哪一种非常靠近底层的东西了,大部分开发者很少直接接触这些东西,想要弄明白这个可以回去请教你们的线性代数老师,这里也仅作概述。
方法 | 摘要 |
---|---|
invert | 求矩阵的逆矩阵 |
isAffine | 判断当前矩阵是否为仿射矩阵,API21(5.0)才添加的方法。 |
isIdentity | 判断当前矩阵是否为单位矩阵。 |
求矩阵的逆矩阵,简而言之就是计算与之前相反的矩阵,如果之前是平移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]
判断矩阵是否是仿射矩阵, 貌似并没有太大卵用,因为你无论如何操作结果始终都为true。
这是为什么呢?因为迄今为止我们使用的所有变换都是仿射变换,那变换出来的矩阵自然是仿射矩阵喽。
判断是否是仿射矩阵最重要的一点就是,直线是否仍为直线,简单想一下就知道,不论平移,旋转,错切,缩放,直线变换后最终仍为直线,要想让isAffine
的结果变为false,除非你能把直线掰弯,我目前还没有找到能够掰弯的方法,所以我仍是直男(就算找到了,我依旧是直男)。
简单测试:
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
判断是否为单位矩阵,什么是单位矩阵呢,就是文章一开始的那个:
新创建的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
通过前面的代码和示例,我们已经了解了Matrix大部分方法是如何使用的,这些基本的原理和方法通过组合可能会创造出神奇的东西,网上有很多教程讲Bitmap利用Matrix变换来制作镜像倒影等,这都属于Matrix的基本应用,我就不在赘述了,下面我简要介绍几种然并卵的小技巧,更多的大家可以开启自己的脑洞来发挥。
在之前的文章Matrix原理中我们提到过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]
这个全凭大家想象力啦,不过我搜了一下还真搜到了好东西,之前鸿洋大大发过一篇博文详细讲解了利用setPolyToPoly制造的折叠效果布局,大家直接到他的博客去看吧,我就不写了。
图片引用自鸿洋大大的博客,稍作了一下处理。
博文链接:
Android FoldingLayout 折叠布局 原理及实现(一)
Android FoldingLayout 折叠布局 原理及实现(二)
本篇基本讲解了Matrix相关的所有方法,应该是目前对Matrix讲解最全面的一篇中文文章了,建议配合上一篇Matrix原理食用效果更佳。
由于本人水平有限,可能出于误解或者笔误难免出错,如果发现有问题或者对文中内容存在疑问欢迎在下面评论区告诉我,请对问题描述尽量详细,以帮助我可以快速找到问题根源。
本系列相关文章
作者微博: GcsSloop
Matrix
Matrix.ScaleToFit
Android中图像变换Matrix的原理、代码验证和应用
Understanding Affine Transformations With Matrix Mathematics
本文内容偏向理论,和 画布操作 有重叠的部分,本文会让你更加深入的了解其中的原理。
本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们所有看到View背后都有着Matrix的功劳,但我们却很少见到它,本篇我们就看看它是何方神圣吧。
由于Google已经对这一部分已经做了很好的封装,所以跳过本部分对实际开发影响并不会太大,不想深究的粗略浏览即可,下一篇中将会详细讲解Matrix的具体用法和技巧。
⚠️ 警告:测试本文章示例之前请关闭硬件加速。
Matrix是一个矩阵,主要功能是坐标映射,数值转换。
它看起来大概是下面这样:
Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:
我的的手机屏幕作为物理设备,其物理坐标系是从左上角开始的,但我们在开发的时候通常不会使用这一坐标系,而是使用内容区的坐标系。
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
以上是仅作为一个简单的示例,实际上不论2D还是3D,我们要将图形显示在屏幕上,都离不开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 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:
我们所用到的变换均属于仿射变换,仿射变换是 线性变换(缩放,旋转,错切) 和 平移变换(平移) 的复合,由于这些概念对于我们作用并不大,此处不过多介绍,有兴趣可自行了解。
基本变换有4种: 平移(translate)、缩放(scale)、旋转(rotate) 和 错切(skew)。
下面我们看一下四种变换都是由哪些参数控制的。
从上图可以看到最后三个参数是控制透视的,这三个参数主要在3D效果中运用,通常为(0, 0, 1),不在本篇讨论范围内,暂不过多叙述,会在之后对文章中详述其作用。
由于我们以下大部分的计算都是基于矩阵乘法规则,如果你已经把线性代数还给了老师,请参考一下这里: 维基百科-矩阵乘法
用矩阵表示:
你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
(x, y, 1) - 点
(x, y, 0) - 向量另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)…(2N,3N,N)表示的均是(2,3)这一个点。(将MPERSP_2解释为scale这一误解就源于此)。
图例:
错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。
用矩阵表示:
图例:
用矩阵表示:
图例:
水平错切和垂直错切的复合。
用矩阵表示:
图例:
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
用矩阵表示:
图例:
此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
用矩阵表示:
图例:
其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
我们常用的四大变换操作,每一种操作在Matrix均有三类,前乘(pre),后乘(post)和设置(set),可以参见文末对Matrix方法表,由于矩阵乘法不满足交换律,所以前乘(pre),后乘(post)和设置(set)的区别还是很大的。
前乘相当于矩阵的右乘:
这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。
前乘相当于矩阵的左乘:
这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。
设置使用的不是矩阵乘法,而是直接覆盖掉原来的数值,所以,使用设置可能会导致之前的操作失效。
关于 Matrix 的文章终有一个问题,就是 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 :先,和 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
从实际上来说,由于矩阵乘法满足结合律,所以不论你说是靠右先执行还是靠左先执行,从结果上来说都没有错。
之前基于这条错误的结论我进行了一次错误的证明:
(这段内容注定要成为我写作历程中不可抹灭的耻辱,既然是公开文章,就应该对读者负责,虽然我在发表每一篇文章之前都竭力的求证其中的问题,各种细节,避免出现这种错误,但终究还是留下了这样一段内容,在此我诚挚的向我所有的读者道歉。)
关注我的读者请尽量看我在 个人博客 和 GitHub 发布的版本,这两个平台都在博文修复计划之内,有任何错误或者纰漏,都会首先修复这两个平台的文章。另外,所有进行修复过的文章都会在我的微博 @GcsSloop 重新发布说明,关注我的微博可以第一时间得到博文更新或者修复的消息。
以下是错误证明:
在实际操作中,我们每一步操作都会得出准确的计算结果,但是为什么还会用存在先后的说法? 难道真的能够用pre和post影响计算顺序? 实则不然,下面我们用一个例子说明:
Matrix matrix = new Matrix(); matrix.postScale(0.5f, 0.8f); matrix.preTranslate(1000, 1000); Log.e(TAG, "MatrixTest" + matrix.toShortString());
在上面的操作中,如果按照正常的思路,先缩放,后平移,缩放操作执行在前,不会影响到后续的平移操作,但是执行结果却发现平移距离变成了(500, 800)。
在上面例子中,计算顺序是没有问题的,先计算的缩放,然后计算的平移,而缩放影响到平移则是因为前一步缩放后的结果矩阵右乘了平移矩阵,这是符合矩阵乘法的运算规律的,也就是说缩放操作虽然在前却影响到了平移操作,相当于先执行了平移操作,然后执行的缩放操作,因此才有pre操作会先执行,而post操作会后执行这一说法。
上面的论证是完全错误的,因为可以轻松举出反例:
Matrix matrix = new Matrix(); matrix.preScale(0.5f, 0.8f); matrix.preTranslate(1000, 1000); Log.e(TAG, "MatrixTest" + matrix.toShortString());
反例中,虽然将
postScale
改为了preScale
,但两者结果是完全相同的,所以先后论根本就是错误的。他们结果相同是因为最终化简公式是相同的,都是 S*T
之所以平移距离是 MTRANS_X = 500,MTRANS_Y = 800,那是因为执行 Translate 之前 Matrix 已经具有了一个缩放比例。在右乘的时候影响到了具体的数值计算,可以用矩阵乘法计算一下。
最终结果为:
当 T*S 的时候,缩放比例则不会影响到 MTRANS_X 和 MTRANS_Y ,具体可以使用矩阵乘法自己计算一遍。
不要去管什么先后论,顺序论,就按照最基本的矩阵乘法理解。
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 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。
注意:
// 使用pre, M' = M*T*S = T*S
Matrix m = new Matrix();
m.reset();
m.preTranslate(tx, ty);
m.preScale(sx, sy);
用矩阵表示:
// 使用post, M‘ = T*S*M = T*S
Matrix m = new Matrix();
m.reset();
m.postScale(sx, sy); //,越靠前越先执行。
m.postTranslate(tx, ty);
用矩阵表示:
// 混合 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);
由于此处只有两步操作,且指定了先后,所以代码上交换并不会影响结果。
用矩阵表示:
注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。
上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。
这个方法表,暂时放到这里让大家看看,方法的使用讲解放在下一篇文章中。
方法类别 | 相关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 isIdentity | 求逆矩阵、 是否为仿射矩阵、 是否为单位矩阵 … |
对于Matrix重在理解,理解了其中的原理之后用起来将会更加得心应手。
学完了本篇之后,推荐配合鸿洋大大的视频课程 打造个性的图片预览与多点触控 食用,定然能够让你对Matrix对理解更上一层楼。
由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 作者微博 不仅能第一时间收到新文章消息,还能变帅哦。
本系列相关文章
作者微博: GcsSloop
Matrix
Android中图像变换Matrix的原理、代码验证和应用
Android中关于矩阵(Matrix)前乘后乘的一些认识
维基百科-仿射变换
维基百科-齐次坐标
维基百科-线性映射
齐次坐标系入门级思考
仿射变换与齐次坐标