一、为什么会有这篇文章
- Matrix 在图像处理方面至关重要
- Matrix 概念相对抽象,不好理解
- 网上博客文档大多尝试深入却无法浅出,新人学习云里雾里,看完依旧不知道怎么用
所以想用这篇文档带大家入个门,看完之后至少能够知道如何正确使用 Matrix,也为后续更深层的学习打下基础
二、初识 Android Matrix
Matrix 中文名:矩阵。(你可能也听过 Transform 这个词,他们本质上是一样的东西,只不过在不同的平台默认锚点可能不同)
说到矩阵,学过线性代数的同学都知道,矩阵其实就是个 m * n 的数组。就像这样:
Android 的 Matrix 的本质是一个 3 * 3 的二维数组,也就是总共有 9 个值记录着所有的信息,包括平移、旋转、缩放、镜像、斜切等信息,且并不是简单的对应关系。打开 Matrix 源码可以看到:
那这 9 个值分别代表什么含义呢?
这篇文档不打算讲,因为它不容易讲清,对于初学者来说过早的了解这些,容易陷入细节深渊,最后放弃。我们直接来讲讲 Matrix 的本质:
Matrix 本质是一种 「变换规则」,或叫 「映射规则」。通过这个规则,你可以将某个点的位置变换到一个新的位置上去。
这样说可能还是不够具体。
假设我们有一个 Matrix,这个 Matrix 内定义了一种平移操作(dx = +100, dy = -100);
我们还有一个点 A(x = 100, y = 100);点 A 经过 Matrix 变换后,就得到 A’(x = 200, y = 0)。
我们都知道线是由点构成的,当我们对一条线上的所有点做 Matrix 变换后,这整条线也就被变换了,所以 Android Path 也是可以使用 Matrix 做变换的。
而面是由线构成的,同理,我们也可以对一个图像做 Matrix 变换。
下面我们就来结合一个 demo 来看一下如何使用 Matrix 做一些简单的变换
三、Matrix 的简单使用
Matrix 最常用的场景是,结合 canvas 来绘制一张图
- 我们自定义一个 MatrixView,用来显示一张图。代码如下:
class MatrixView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// 声明并创建一个矩阵,默认是单位矩阵
private val m = Matrix()
// 用来变换的图片
private val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pikaqiu)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// TODO .. 在这里操作矩阵
canvas.drawBitmap(bitmap, m, null)
}
}
- 把 MatrixView 放在 activity 的根布局中,并给它的宽高设置 “match_parent”,这样它就占满了整个 activity。(代码略)
- 运行后,我们可以看到,当 Matrix 为单位矩阵时,图片左上角与控件左上角对齐
- 如果想要让图片中心点与控件中心点对齐,再以控件中心点为锚点放大 1.8 倍,要怎么做呢?
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// TODO .. 在这里操作矩阵
m.reset() // 重置
m.postTranslate((width - bitmap.width) / 2f, (height - bitmap.height) / 2f) // 平移使其居中
m.postScale(1.8f, 1.8f, width / 2f, height / 2f) // 以控件中心点做锚点放大
canvas.drawBitmap(bitmap, m, null)
}
为了方便理解,以下是分步示例图:平移居中后再放大。(但实际是构建好矩阵后,将其传入 canvas.drawBitmap() 才会画出「皮卡丘」,并非操作矩阵时就发生作用)
注意:这里我们操作 Matrix,只是对图片渲染的位置进行变换,坐标轴没有发生变换!而操作 Canvas 坐标轴就会发生变换!
- 是否还有其他方式,可以实现上述效果呢?
比如,先放大,再平移?
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// TODO .. 在这里操作矩阵
m.reset() // 重置
val scale = 1.8f
m.postScale(scale, scale) // 默认以原点为锚点进行放大(旋转、缩放方法也如此,没有传入锚点,默认以 Canvas原点 为锚点)
m.postTranslate((width - scale * bitmap.width) / 2f, (height - scale * bitmap.height) / 2f) // 平移使其居中
canvas.drawBitmap(bitmap, m, null)
}
依旧是分步示例图~
和上一种方式对比,为什么 Translate 的值不一样了?
因为经过 postScale(scale, scale) 后,内容被放大了,内容被放大了,内容被放大了,我们要对已经被放大的内容做居中处理。这里之所以强调「内容」,我们在做 Matrix 变换的时候,心里或纸上一定要有一张草图,记住「内容」的当前状态(包括位置、大小、旋转角度等),基于此再做「增量」的变换操作。
结合(4)(5)两点,我们也可以知道矩阵操作是不支持「交换律」的。(多说一句:矩阵操作支持结合律)
- 除了 postScale(sx, sy) 缩放、postTranslate(dx, dy) 平移。Matrix 还支持 postRotate(degree) 旋转,postSkew(kx, ky) 斜切,postConcat(matrix) 应用一个矩阵 这几个基本操作,使用规则和平移缩放类似,自行了解即可,不再赘述
四、Post vs Pre
Android 还给我们提供了以 ‘pre’ 开头的方法,和 ‘post’ 开头的方法一一对应。
这两类方法分别对应着 矩阵的 前乘 和 后乘,它们的效果是不一样的,因为不支持交换律。
(但当你基于一个 单位矩阵 preRotate(90) 或 postRotate(90),你会发现他们的效果是一致的。因为单位矩阵 M 无论前乘还是后乘一个矩阵 M’,都会等于 M’,这算是特例。)
简单理解:
- 对于已有 Matrix 做 ‘post’ 操作,就是在已有 Matrix 的基础上 追加 ‘post’ 变换
- 而对于已有 Matrix 做 ‘pre’ 操作,是以 ‘pre’ 变换为基准,在此之上 追加 已有 Matrix 变换
这两者的效果是一样的:
m.reset() // 重置
m.postTranslate((width - bitmap.width) / 2f, (height - bitmap.height) / 2f) // 平移使其居中
m.postScale(1.8f, 1.8f, width / 2f, height / 2f) // 以控件中心点做锚点放大
m.reset() // 重置
m.preScale(1.8f, 1.8f, width / 2f, height / 2f) // 以控件中心点做锚点放大
m.preTranslate((width - bitmap.width) / 2f, (height - bitmap.height) / 2f) // 平移使其居中
大多数情况,推荐使用 ‘post’ 类的操作,它更符合思维习惯,一步一步的处理。而 ‘pre’ 就像倒着走,不推荐。非常不推荐 ‘post’ ‘pre’ 混着用,很容易把自己绕晕
五、Matrix 的锚点
若调用 postScale(float sx, float sy) 这个方法,锚点默认是坐标系原点。
android.graphics.Matrix.java
/**
* Postconcats the matrix with the specified scale. M' = S(sx, sy, px, py) * M
*/
public boolean postScale(float sx, float sy, float px, float py) {
nPostScale(native_instance, sx, sy, px, py);
return true;
}
/**
* Postconcats the matrix with the specified scale. M' = S(sx, sy) * M
*/
public boolean postScale(float sx, float sy) {
nPostScale(native_instance, sx, sy);
return true;
}
如何通过它基于某个锚点做变换呢?
val px = width / 2f
val py = height / 2f
// 方法1
m.postTranslate(-px, -py)
m.postScale(1.8f, 1.8f) // 缩放操作
m.postTranslate(px, py)
// 方法2,与方法1效果一致
// m.postScale(1.8f, 1.8f, px, py)
有人可能会问,那我直接调用「方法2」不就好了,为什么要学习「方法1」?
m.postTranslate(-px, -py)
...(任意 'post' 操作)
m.postTranslate(px, py)
这种模板适用于任意 ‘post’ 操作,含义是:以(px,py)为锚点,进行某种变换。
像 postConcat(Matrix other) 没有提供「基于某个锚点应用矩阵」方法的,只能通过这种方式去实现。
六、其他方法
点的变换:
/**
* Apply this matrix to the array of 2D points specified by src, and write the transformed
* points into the array of points specified by dst. The two arrays represent their "points" as
* pairs of floats [x, y].
*/
public void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex,
int pointCount) {
...
}
Rect 的变换:(若 Matrix 内有旋转变换,Rect 会是旋转后矩形的包围盒,因为 Rect 始终描述的都是一个正的矩形)
/**
* Apply this matrix to the src rectangle, and write the transformed rectangle into dst. This is
* accomplished by transforming the 4 corners of src, and then setting dst to the bounds of
* those points.
*/
public boolean mapRect(RectF dst, RectF src) {
...
}
逆矩阵:矩阵和它的逆矩阵相乘,得到的结果是单位矩阵。可以用来做矩阵的抵消
/**
* If this matrix can be inverted, return true and if inverse is not null, set inverse to be the
* inverse of this matrix. If this matrix cannot be inverted, ignore inverse and return false.
*/
public boolean invert(Matrix inverse) {
return nInvert(native_instance, inverse.native_instance);
}
判断是否是单位矩阵:
public boolean isIdentity() {
return nIsIdentity(native_instance);
}
重置为单位矩阵:
public void reset() {
nReset(native_instance);
}
设置缩放(会清除之前的所有变换):
/** Set the matrix to scale by sx and sy. */
public void setScale(float sx, float sy) {
nSetScale(native_instance, sx, sy);
}
根据 源 src 和 目标 dst,计算并设置矩阵。用来反推矩阵:
/**
* Set the matrix to the scale and translate values that map the source rectangle to the
* destination rectangle, returning true if the the result can be represented.
*/
public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
...
}
/**
* Set the matrix such that the specified src points would map to the specified dst points. The
* "points" are represented as an array of floats, order [x0, y0, x1, y1, ...], where each
* "point" is 2 float values.
*/
public boolean setPolyToPoly(float[] src, int srcIndex,
float[] dst, int dstIndex,
int pointCount) {
..
}
七、结尾
到这里,矩阵的基本用法就讲完了。
虽然我在前文有提到不要过早的去了解细节,但想要更加深入、真正掌握,还是要知道其运行的原理,知其然知其所以然~
祝大家学习进步,日进斗金~