ScaleGestureRecognizer:利用 `focalPoint` 计算缩放矩阵的数学推导

ScaleGestureRecognizer:利用 focalPoint 计算缩放矩阵的数学推导

大家好,今天我们深入探讨 ScaleGestureRecognizer 中一个关键环节:如何利用 focalPoint(焦点)计算缩放矩阵。这部分是实现平滑且自然的缩放体验的核心。我们将从数学原理出发,结合代码示例,逐步剖析其中的细节。

1. 缩放矩阵的基础

在理解 focalPoint 的作用之前,我们需要先掌握缩放矩阵的基本概念。一个标准的 2D 缩放矩阵如下所示:

| Sx  0  0 |
| 0  Sy  0 |
| 0  0  1 |

其中,Sx 代表 X 轴方向的缩放比例,Sy 代表 Y 轴方向的缩放比例。如果 SxSy 相等,则为均匀缩放;否则为非均匀缩放。将这个矩阵应用于一个点 (x, y),得到缩放后的点 (x', y')

x' = Sx * x
y' = Sy * y

这个缩放是以原点 (0, 0) 为中心的。但是,在手势操作中,我们通常希望以手指触摸的中心点(即 focalPoint)为中心进行缩放。这就需要进行坐标变换。

2. 以 focalPoint 为中心的缩放

为了以 focalPoint 为中心进行缩放,我们需要执行以下三个步骤:

  1. 平移(Translation): 将坐标系原点平移到 focalPoint
  2. 缩放(Scaling): 以新的原点(即原来的 focalPoint)为中心进行缩放。
  3. 反向平移(Inverse Translation): 将坐标系原点平移回原来的位置。

这三个步骤可以用矩阵运算表示如下:

  • 平移矩阵 T(tx, ty):

    | 1  0  tx |
    | 0  1  ty |
    | 0  0  1  |

    其中 txty 分别是 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     |

在这个矩阵中,SxSy 是缩放比例,(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 缩放到超出屏幕范围。
  • 精度问题: 在进行矩阵运算时,可能会出现精度问题。 可以使用 floatdouble 类型来提高精度。
  • 与其他变换的组合: 缩放矩阵可以与其他变换矩阵(例如平移、旋转)组合使用,实现更复杂的变换效果。 注意矩阵相乘的顺序,不同的顺序会导致不同的结果。

6. 高级应用:多点触控和手势识别

ScaleGestureRecognizer 是多点触控和手势识别的基础。通过结合其他手势识别器(例如 GestureDetector),可以实现更复杂的手势交互。 例如,可以实现双指缩放、单指拖动、双指旋转等功能。

表格:矩阵运算的总结

矩阵类型 矩阵形式 描述
缩放矩阵 S(Sx, Sy) | Sx 0 0 | | 0 Sy 0 | | 0 0 1 | 以原点为中心进行缩放,SxSy 分别是 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 的内部实现、性能优化、锚点选择和边界处理等问题。掌握这些知识,可以帮助开发者实现更平滑、自然的缩放体验。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注