You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Canvas双指缩放平移且保持绘制比例、支持无限画布?

解决Canvas双指缩放不丢比例+无限画布方案

嘿,我来帮你搞定这个问题!你已经实现了手指绘制,现在要解决双指缩放比例丢失和无限画布的需求,核心思路是用矩阵(Matrix)来统一管理画布的缩放、平移变换,让所有绘制操作都基于原始的无限坐标系统,而不是屏幕坐标。

核心原理

  1. 矩阵管理变换:用一个Matrix对象保存画布的缩放、平移状态,绘制时将这个矩阵应用到Canvas上,这样所有绘制内容都会自动跟着变换,不会出现比例丢失。
  2. 触摸坐标转换:因为画布有变换,单指绘制时需要把屏幕触摸坐标通过逆矩阵转换为原始画布的坐标,确保绘制的路径是基于无限画布的,不受缩放平移影响。
  3. 双指缩放逻辑:以双指中心点为基准缩放,同时处理平移,保证缩放时中心点固定,平移速度和缩放比例匹配。

修改后的完整代码示例

首先更新你的DrawingView类,集成手势检测和矩阵变换:

public class DrawingView extends View {
    private Paint mPaint;
    private Path mDrawPath;
    // 管理画布缩放、平移的核心矩阵
    private Matrix mCanvasMatrix = new Matrix();
    // 用于将屏幕坐标转换为原始画布坐标的逆矩阵
    private Matrix mInverseMatrix = new Matrix();
    // 缩放手势检测器
    private ScaleGestureDetector mScaleDetector;
    // 当前缩放比例
    private float mCurrentScale = 1.0f;
    // 双指缩放时的上次中心点(屏幕坐标)
    private float mLastScaleCenterX, mLastScaleCenterY;
    // 单指绘制的上次点(原始画布坐标)
    private float mLastDrawX, mLastDrawY;
    // 是否正在进行缩放操作
    private boolean isScaling = false;

    public DrawingView(Context context) {
        super(context);
        init(context);
    }

    public DrawingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        // 初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        // 这里的笔触宽度如果要固定视觉大小,后续可以根据缩放比例动态调整
        mPaint.setStrokeWidth(5);

        mDrawPath = new Path();
        // 初始化缩放手势检测器
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 优先处理缩放手势
        mScaleDetector.onTouchEvent(event);

        if (!isScaling) {
            // 非缩放状态下处理绘制逻辑
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 把屏幕触摸点转换为原始画布坐标
                    float[] downPoint = {event.getX(), event.getY()};
                    mCanvasMatrix.invert(mInverseMatrix);
                    mInverseMatrix.mapPoints(downPoint);
                    mLastDrawX = downPoint[0];
                    mLastDrawY = downPoint[1];
                    mDrawPath.moveTo(mLastDrawX, mLastDrawY);
                    return true;
                case MotionEvent.ACTION_MOVE:
                    float[] movePoint = {event.getX(), event.getY()};
                    mCanvasMatrix.invert(mInverseMatrix);
                    mInverseMatrix.mapPoints(movePoint);
                    mDrawPath.lineTo(movePoint[0], movePoint[1]);
                    mLastDrawX = movePoint[0];
                    mLastDrawY = movePoint[1];
                    invalidate();
                    return true;
                case MotionEvent.ACTION_UP:
                    invalidate();
                    return true;
            }
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 保存画布当前状态,应用变换矩阵
        canvas.save();
        canvas.concat(mCanvasMatrix);
        // 绘制路径(路径是基于原始无限画布的坐标,矩阵会自动处理缩放平移)
        canvas.drawPath(mDrawPath, mPaint);
        // 恢复画布状态
        canvas.restore();
    }

    // 缩放手势监听类
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            isScaling = true;
            // 记录缩放开始时的双指中心点
            mLastScaleCenterX = detector.getFocusX();
            mLastScaleCenterY = detector.getFocusY();
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // 计算本次缩放比例
            float scaleFactor = detector.getScaleFactor();
            // 限制缩放范围(可选,防止缩得太小或太大)
            mCurrentScale = Math.max(0.1f, Math.min(mCurrentScale * scaleFactor, 10.0f));

            // 获取双指中心点的原始画布坐标,确保缩放时中心点位置不变
            float[] focusPoint = {detector.getFocusX(), detector.getFocusY()};
            mCanvasMatrix.invert(mInverseMatrix);
            mInverseMatrix.mapPoints(focusPoint);

            // 构建缩放变换矩阵:以中心点为基准缩放
            Matrix tempMatrix = new Matrix();
            tempMatrix.postTranslate(-focusPoint[0], -focusPoint[1]);
            tempMatrix.postScale(scaleFactor, scaleFactor);
            tempMatrix.postTranslate(focusPoint[0], focusPoint[1]);
            mCanvasMatrix.postConcat(tempMatrix);

            // 处理双指平移:如果双指在缩放时移动,同步平移画布
            float dx = detector.getFocusX() - mLastScaleCenterX;
            float dy = detector.getFocusY() - mLastScaleCenterY;
            if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
                // 屏幕平移量需要除以当前缩放比例,转换为原始画布的平移量
                mCanvasMatrix.postTranslate(dx / mCurrentScale, dy / mCurrentScale);
                mLastScaleCenterX = detector.getFocusX();
                mLastScaleCenterY = detector.getFocusY();
            }

            invalidate();
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            isScaling = false;
        }
    }
}

关键细节说明

  • 无限画布实现:我们的绘制路径mDrawPath保存的是原始的无限坐标点,Canvas只是通过矩阵变换展示其中的一部分,理论上可以无限绘制,只要设备内存足够存储路径点。
  • 笔触宽度控制:如果希望笔触宽度不随缩放变化(视觉上始终一样粗),可以在onDraw里动态设置:mPaint.setStrokeWidth(5 / mCurrentScale);如果希望笔触跟着缩放变粗/细,就保持固定值即可。
  • 缩放范围限制:示例里限制了缩放比例在0.1到10倍之间,你可以根据需求调整这个范围。
  • 触摸事件优先级:优先处理缩放手势,避免绘制和缩放操作冲突。

内容的提问来源于stack exchange,提问作者Makhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:42:31