Android | Matrix 类设计与核心逻辑解析

266 阅读7分钟

矩阵计算规则

因为矩阵相关知识点早就还给了老师,所以在开始前,先复习一下矩阵的计算规则:第一个矩阵第一行的每个数字,各自乘以第二个矩阵第一列对应位置的数字,然后将乘积相加,得到结果矩阵左上角的那个值,以此类推。如: 矩阵计算规则 矩阵的本质就是线性方程式,两者是一一对应关系: 线性方程

关于矩阵的计算,可以参见这篇文章:5分钟让你彻底理解矩阵乘法

Matrix 常用方法及示例

Matrix 是 Android 中图形系统的核心类,广泛用于 Canvas 绘制、View 变换、动画、手势处理等场景。Matrix 表示一个 3x3 的矩阵,专门用于 2D 图形变换:

| MSCALE_X  MSKEW_X   MTRANS_X |
| MSKEW_Y   MSCALE_Y  MTRANS_Y |
| MPERSP_0  MPERSP_1  MPERSP_2 |

前两行用于仿射变换(平移、旋转、缩放、错切),第三行用于透视变换(一般场景用不到)。

Matrix构造方法

public Matrix() {
  native_instance = nCreate(0);
  NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
}

Matrix 的核心计算由 JNI 实现,依赖 Native 层(C/C++)进行高性能变换,借助 NativeAllocationRegistry 自动释放 native 内存,避免内存泄漏。此外,Matrix 还提供一系列前置 / 后置 / 设置型方法:

setTranslate() / preTranslate() / postTranslate()  //平移
setScale()     / preScale()     / postScale()  //缩放
setRotate()    / preRotate()    / postRotate() //旋转
setSkew()      / preSkew()      / postSkew() //错切
  • preXxx()右乘: M' = M * X
  • postXxx()左乘: M' = X * M
  • setXxx(): M' = X

⚠️ 注意:矩阵乘法不满足交换律,顺序会影响最终变换效果

1、setTranslate(float dx, float dy) 设置平移矩阵:

Matrix matrix = new Matrix();
matrix.setTranslate(100, 50);

表示所有点都向右移动 100 像素,向下移动 50 像素。

2、setScale(float sx, float sy)、setScale(float sx, float sy, float px, float py) 设置缩放变换:

matrix.setScale(2f, 2f); // 以原点为中心放大 2 倍
matrix.setScale(2f, 2f, 100, 100); // 以点 (100, 100) 为中心缩放

3、setRotate(float degrees)、setRotate(float degrees, float px, float py) 设置旋转变换:

matrix.setRotate(45); // 以原点旋转 45 度
matrix.setRotate(45, 100, 100); // 以点 (100, 100) 为中心旋转

4、setSkew(float kx, float ky) 、setSkew(float kx, float ky, float px, float py) 设置倾斜变换:

matrix.setSkew(1f, 0f); // 水平倾斜
matrix.setSkew(0f, 1f, 50, 50); // 以点 (50,50) 为中心垂直倾斜

5、setConcat(Matrix a, Matrix b) 将当前 Matrix 设置为两个矩阵相乘的结果:this = a × b,如:

Matrix a = new Matrix();
a.setRotate(45);

Matrix b = new Matrix();
b.setScale(2f, 2f);

Matrix result = new Matrix();
result.setConcat(a, b); // 先缩放再旋转

此外,还有 postConcat(Matrix other) 会将一个变换矩阵“后乘”到当前矩阵的方法;而preConcat则是将变换矩阵前乘到当前矩阵。

方法数学表示实际含义
setConcat(a, b)this = a × b把 a 和 b 的组合结果赋值给 this
preConcat(b)this = this × b相当于后乘
postConcat(b)this = b x this相当于前乘

6、postTranslate / postScale / postRotate / postSkew / postConcat 这些是「后乘法」,变换会追加到当前矩阵之后:

matrix.postTranslate(100, 0);  // M = T * M
matrix.postScale(2f, 2f);      // M = S * M

7、preTranslate / preScale / preRotate / preSkew / preConcat 这些是「前乘法」,变换会插入到当前矩阵之前:

matrix.preTranslate(100, 0);  // M = M * T
matrix.preScale(2f, 2f);      // M = M * S

8、mapPoints(float[] pts) 将点数组映射到新坐标(数组个数必须是偶数,如果是奇数,最后一个参数不会参与运算):

float[] pts = {0, 0, 100, 100};
matrix.setTranslate(50, 50);
matrix.mapPoints(pts);
// pts = {50, 50, 150, 150}

9、mapRect(RectF rect) 矩形坐标变换:

RectF rect = new RectF(0, 0, 100, 100);
matrix.setScale(2, 2);
matrix.mapRect(rect);
// rect = (0, 0, 200, 200)

10、mapVectors(float[] vectors) 不考虑平移,只映射方向向量(如用于图像法线、速度方向等),这也是与mapPoints的区别,示例:

val matrix = Matrix().apply {
    setRotate(90f)
    postTranslate(100f, 200f)
}
val point = floatArrayOf(10f, 0f)
matrix.mapPoints(point)
// 输出: [100.0, 210.0] => (10,0) 先旋转得到(0,10),再加上平移(100,200)
log(point.contentToString())

val samePoint = floatArrayOf(10f, 0f)
matrix.mapVectors(samePoint)
// 输出: [0.0, 10.0] => 表示向量原地旋转90°,忽略平移
log(samePoint.contentToString())

11、setPolyToPoly 根据src源点和dst目标点的对应关系得到一个变换矩阵,并用这个矩阵对坐标或位图做变换(平移/旋转/缩放/错切/透视等)。

public void setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
  • src:源点数组,格式 x0, y0, x1, y1, ...
  • srcIndex:src 起始偏移,通常为 0。
  • dst:目标点数组,格式同 src。
  • dstIndex:dst 起始偏移,通常为 0。
  • pointCount:使用多少对点来求变换(只能是 0,1,2,3,4)。

关于setPolyToPoly的详细使用参见:Android | Matrix.setPolyToPoly() 图像变换详解

13、getValues / setValues 直接获取或设置矩阵数组(9 个 float 值):

float[] values = new float[9];
matrix.getValues(values);

values[Matrix.MTRANS_X] += 100;
matrix.setValues(values);
常量名索引描述
MSCALE_X0水平方向缩放因子
MSKEW_X1水平倾斜因子
MTRANS_X2X 平移值
MSKEW_Y3垂直倾斜因子
MSCALE_Y4垂直缩放因子
MTRANS_Y5Y 平移值
MPERSP_0-26~8透视变换(一般为 0, 0, 1)

14、setRectToRect: 用于两个矩形之间的映射(缩放 + 对齐)。

public boolean setRectToRect(
    RectF src,      // 源矩形
    RectF dst,      // 目标矩形
    ScaleToFit stf  // 缩放适配模式
)

其中 src 定义原始内容的边界,dst 定义变换后内容的边界,Matrix.ScaleToFit 表示指定矩形适配的缩放模式,分别为:

  • FILL:拉伸填充整个目标区域,可能改变宽高比
  • START:保持宽高比缩放,对齐到目标区域的左上角
  • CENTER:保持宽高比缩放,在目标区域居中显示
  • END:保持宽高比缩放,对齐到目标区域的右下角

Matrix.ScaleToFit 使用示例:

//原始图片边界
val srcRect = RectF(0f, 0f, image.getWidth(), image.getHeight())
//目标显示区域
val dstRect = RectF(0f, 0f, viewWidth, viewHeight)
//创建居中适配矩阵
val matrix = Matrix()
matrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.CENTER)
//应用矩阵绘制图片
canvas.drawBitmap(image, matrix, paint)

典型用途场景

用途示例代码
Canvas 变换,可以在自定义View绘制中使用canvas.concat(matrix)
图片变换imageView.setImageMatrix(matrix)
手势缩放手势缩放/旋转过程中修改 Matrix 实现图像动态变换

1、Canvas变换:Canvas.concat(Matrix matrix)

该方法用于将指定的矩阵与当前画布的变换矩阵相乘,影响后续的绘图操作。在自定义绘图时,使用 canvas.concat(matrix) 可以对画布进行变换,如旋转、缩放、平移等。示例

Matrix matrix = new Matrix();
matrix.setRotate(45, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0, 0, paint);
canvas.restore();

此代码将画布旋转 45 度,然后绘制位图。

2、图片变换:ImageView.setImageMatrix(Matrix matrix)

该方法用于设置 ImageView 的图像变换矩阵,需将 scaleType 设置为 MATRIX。通过设置图像矩阵,可以实现图像的缩放、旋转、平移等效果。示例

private val mIvImg: ImageView by id(R.id.iv_matrix)

/**
 * 利用Matrix模拟FIT_XY的效果
 */
mIvImg.post {
    val drawable = mIvImg.drawable
    drawable?.let {
        val matrix = Matrix()
        val widthScale = mIvImg.width.toFloat() / it.intrinsicWidth
        val heightScale = mIvImg.height.toFloat() / it.intrinsicHeight
        matrix.setScale(widthScale, heightScale)
        matrix.postTranslate(0f, 0f)
        mIvImg.imageMatrix = matrix
    }
}

上述代码利用Matrix实现出了FIT_XY的效果,还可以按需实现其他效果。

3、手势处理:缩放/旋转手势操作时修改 Matrix 实现图像变换

通过GesturerDetector + Matrix 监听手势事件,动态修改矩阵,实现图像的缩放和旋转。实现图像的手势缩放和旋转功能。示例参见:GestureDetector + Matrix 实现图片拖动、缩放与旋转等功能