Android Matrix 入门(可能是东半球最简单的教程)

一、为什么会有这篇文章

  1. Matrix 在图像处理方面至关重要
  2. Matrix 概念相对抽象,不好理解
  3. 网上博客文档大多尝试深入却无法浅出,新人学习云里雾里,看完依旧不知道怎么用
    所以想用这篇文档带大家入个门,看完之后至少能够知道如何正确使用 Matrix,也为后续更深层的学习打下基础

二、初识 Android Matrix

Matrix 中文名:矩阵。(你可能也听过 Transform 这个词,他们本质上是一样的东西,只不过在不同的平台默认锚点可能不同)

说到矩阵,学过线性代数的同学都知道,矩阵其实就是个 m * n 的数组。就像这样:
m*n数组
Android 的 Matrix 的本质是一个 3 * 3 的二维数组,也就是总共有 9 个值记录着所有的信息,包括平移、旋转、缩放、镜像、斜切等信息,且并不是简单的对应关系。打开 Matrix 源码可以看到:
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 来绘制一张图

  1. 我们自定义一个 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)
    }
}
  1. 把 MatrixView 放在 activity 的根布局中,并给它的宽高设置 “match_parent”,这样它就占满了整个 activity。(代码略)
  2. 运行后,我们可以看到,当 Matrix 为单位矩阵时,图片左上角与控件左上角对齐
    在这里插入图片描述
  3. 如果想要让图片中心点与控件中心点对齐,再以控件中心点为锚点放大 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 坐标轴就会发生变换!

  1. 是否还有其他方式,可以实现上述效果呢?
    比如,先放大,再平移?
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)两点,我们也可以知道矩阵操作是不支持「交换律」的。(多说一句:矩阵操作支持结合律)

  1. 除了 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) {
    ..
}

七、结尾

到这里,矩阵的基本用法就讲完了。

虽然我在前文有提到不要过早的去了解细节,但想要更加深入、真正掌握,还是要知道其运行的原理,知其然知其所以然~
祝大家学习进步,日进斗金~

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值