ScaleGestureRecognizer:利用 focalPoint 计算缩放矩阵的数学推导
大家好,今天我们深入探讨 ScaleGestureRecognizer 中一个关键环节:如何利用 focalPoint(焦点)计算缩放矩阵。这部分是实现平滑且自然的缩放体验的核心。我们将从数学原理出发,结合代码示例,逐步剖析其中的细节。
1. 缩放矩阵的基础
在理解 focalPoint 的作用之前,我们需要先掌握缩放矩阵的基本概念。一个标准的 2D 缩放矩阵如下所示:
| Sx 0 0 |
| 0 Sy 0 |
| 0 0 1 |
其中,Sx 代表 X 轴方向的缩放比例,Sy 代表 Y 轴方向的缩放比例。如果 Sx 和 Sy 相等,则为均匀缩放;否则为非均匀缩放。将这个矩阵应用于一个点 (x, y),得到缩放后的点 (x', y'):
x' = Sx * x
y' = Sy * y
这个缩放是以原点 (0, 0) 为中心的。但是,在手势操作中,我们通常希望以手指触摸的中心点(即 focalPoint)为中心进行缩放。这就需要进行坐标变换。
2. 以 focalPoint 为中心的缩放
为了以 focalPoint 为中心进行缩放,我们需要执行以下三个步骤:
- 平移(Translation): 将坐标系原点平移到
focalPoint。 - 缩放(Scaling): 以新的原点(即原来的
focalPoint)为中心进行缩放。 - 反向平移(Inverse Translation): 将坐标系原点平移回原来的位置。
这三个步骤可以用矩阵运算表示如下:
-
平移矩阵 T(tx, ty):
| 1 0 tx | | 0 1 ty | | 0 0 1 |其中
tx和ty分别是 X 轴和 Y 轴的平移量。 -
缩放矩阵 S(Sx, Sy): (同上)
-
反向平移矩阵 T(-tx, -ty):
| 1 0 -tx | | 0 1 -ty | | 0 0 1 |
将这三个矩阵按顺序相乘,得到最终的变换矩阵 M:
M = T(-tx, -ty) * S(Sx, Sy) * T(tx, ty)
展开这个矩阵乘法,得到:
M = | Sx 0 tx*(1-Sx) |
| 0 Sy ty*(1-Sy) |
| 0 0 1 |
在这个矩阵中,Sx 和 Sy 是缩放比例,(tx, ty) 是 focalPoint 的坐标。这个矩阵可以将任意点 (x, y) 变换成以 focalPoint 为中心缩放后的点 (x', y')。
3. 代码实现(Java & Android)
下面我们用 Java 代码来实现这个缩放矩阵的计算,并应用于 Android 中的 Matrix 类。
import android.graphics.Matrix;
public class ScaleMatrixCalculator {
/**
* 根据缩放比例和焦点计算缩放矩阵
*
* @param scaleX X轴方向的缩放比例
* @param scaleY Y轴方向的缩放比例
* @param focalX 焦点X坐标
* @param focalY 焦点Y坐标
* @return 缩放矩阵
*/
public static Matrix calculateScaleMatrix(float scaleX, float scaleY, float focalX, float focalY) {
Matrix matrix = new Matrix();
// 平移到焦点
matrix.postTranslate(focalX, focalY);
// 缩放
matrix.postScale(scaleX, scaleY);
// 平移回原位
matrix.postTranslate(-focalX, -focalY);
return matrix;
}
/**
* 直接计算矩阵元素的方式,避免多次矩阵相乘带来的性能损耗
* @param scaleX X轴方向的缩放比例
* @param scaleY Y轴方向的缩放比例
* @param focalX 焦点X坐标
* @param focalY 焦点Y坐标
* @return 缩放矩阵
*/
public static Matrix calculateScaleMatrixOptimized(float scaleX, float scaleY, float focalX, float focalY) {
Matrix matrix = new Matrix();
float[] values = {
scaleX, 0, focalX * (1 - scaleX),
0, scaleY, focalY * (1 - scaleY),
0, 0, 1
};
matrix.setValues(values);
return matrix;
}
}
使用示例:
// 获取缩放比例和焦点坐标 (假设这些值来自 ScaleGestureDetector)
float scaleFactor = scaleGestureDetector.getScaleFactor();
float focalX = scaleGestureDetector.getFocusX();
float focalY = scaleGestureDetector.getFocusY();
// 计算缩放矩阵
Matrix scaleMatrix = ScaleMatrixCalculator.calculateScaleMatrix(scaleFactor, scaleFactor, focalX, focalY);
// 将缩放矩阵应用到 View 的 Canvas 上
canvas.concat(scaleMatrix);
// 或者应用到 Bitmap 上,先创建一个新的 Bitmap
Matrix matrix = new Matrix();
matrix.postScale(scaleFactor, scaleFactor, focalX, focalY); //这个是直接用api,更简洁
Bitmap scaledBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true);
// 使用优化后的方法
Matrix optimizedScaleMatrix = ScaleMatrixCalculator.calculateScaleMatrixOptimized(scaleFactor, scaleFactor, focalX, focalY);
canvas.concat(optimizedScaleMatrix);
在这个例子中,我们首先获取 ScaleGestureDetector 提供的缩放比例和焦点坐标。然后,我们使用 ScaleMatrixCalculator 类中的 calculateScaleMatrix 方法计算出缩放矩阵。最后,我们将这个矩阵应用到 Canvas 上,实现 View 的缩放效果。 calculateScaleMatrixOptimized 方法避免了矩阵相乘,更高效。
4. ScaleGestureDetector 的内部实现 (简化)
虽然我们无法完全了解 ScaleGestureDetector 的内部实现,但我们可以推测其大致流程。 ScaleGestureDetector 会监听触摸事件,并根据触摸点的变化计算出缩放比例和焦点坐标。
一个简化的伪代码如下:
class SimplifiedScaleGestureDetector {
private float scaleFactor;
private float focalX;
private float focalY;
public void onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 记录第一个触摸点
break;
case MotionEvent.ACTION_POINTER_DOWN:
// 记录第二个触摸点,开始计算缩放比例和焦点
recalculateScaleFactorAndFocalPoint(event);
break;
case MotionEvent.ACTION_MOVE:
// 根据触摸点的变化,更新缩放比例和焦点
recalculateScaleFactorAndFocalPoint(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 重置状态
break;
}
}
private void recalculateScaleFactorAndFocalPoint(MotionEvent event) {
// 计算两个触摸点之间的距离
float distanceCurrent = calculateDistance(event);
// 如果是初始状态,记录初始距离
if (initialDistance == 0) {
initialDistance = distanceCurrent;
}
// 计算缩放比例
scaleFactor = distanceCurrent / initialDistance;
// 计算焦点坐标 (简单版本,取两个触摸点的中心)
focalX = (event.getX(0) + event.getX(1)) / 2;
focalY = (event.getY(0) + event.getY(1)) / 2;
}
private float calculateDistance(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
public float getScaleFactor() {
return scaleFactor;
}
public float getFocusX() {
return focalX;
}
public float getFocusY() {
return focalY;
}
}
这个伪代码只是一个简化版本,实际的 ScaleGestureDetector 可能会考虑更多的因素,例如:
- 过滤掉抖动和噪声。
- 处理多个触摸点。
- 使用更复杂的算法计算焦点坐标。
5. 优化和注意事项
- 性能优化: 避免在
onDraw方法中频繁创建Matrix对象。 尽可能重用Matrix对象。 使用calculateScaleMatrixOptimized方法直接设置矩阵元素。 - 锚点选择:
focalPoint的选择至关重要。 选择合适的锚点可以使缩放更加自然。 例如,可以根据触摸点的分布动态调整锚点的位置。 - 边界处理: 在缩放过程中,需要考虑边界情况,避免 View 缩放到超出屏幕范围。
- 精度问题: 在进行矩阵运算时,可能会出现精度问题。 可以使用
float或double类型来提高精度。 - 与其他变换的组合: 缩放矩阵可以与其他变换矩阵(例如平移、旋转)组合使用,实现更复杂的变换效果。 注意矩阵相乘的顺序,不同的顺序会导致不同的结果。
6. 高级应用:多点触控和手势识别
ScaleGestureRecognizer 是多点触控和手势识别的基础。通过结合其他手势识别器(例如 GestureDetector),可以实现更复杂的手势交互。 例如,可以实现双指缩放、单指拖动、双指旋转等功能。
表格:矩阵运算的总结
| 矩阵类型 | 矩阵形式 | 描述 |
|---|---|---|
| 缩放矩阵 S(Sx, Sy) | | Sx 0 0 | | 0 Sy 0 | | 0 0 1 | |
以原点为中心进行缩放,Sx 和 Sy 分别是 X 轴和 Y 轴的缩放比例。 |
| 平移矩阵 T(tx, ty) | | 1 0 tx | | 0 1 ty | | 0 0 1 | |
将坐标系原点平移 (tx, ty)。 |
| 反向平移矩阵 T(-tx, -ty) | | 1 0 -tx | | 0 1 -ty | | 0 0 1 | |
将坐标系原点平移 (-tx, -ty)。 |
| 组合矩阵 M | | Sx 0 tx*(1-Sx) | | 0 Sy ty*(1-Sy) | | 0 0 1 | |
以 (tx, ty) 为中心进行缩放,等价于先平移到 (tx, ty),再缩放,再反向平移回原点。 |
缩放矩阵计算:核心步骤和优化
本文深入探讨了如何使用 focalPoint 计算缩放矩阵,介绍了其背后的数学原理和 Java 代码实现。同时,还讨论了 ScaleGestureDetector 的内部实现、性能优化、锚点选择和边界处理等问题。掌握这些知识,可以帮助开发者实现更平滑、自然的缩放体验。