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

Flutter Canvas库旋转对象缩放位置偏移问题求助

问题解决:旋转后Canvas对象缩放位置偏移修复

核心问题根源

你的代码直接使用**屏幕坐标系的鼠标偏移量(delta)**计算缩放和位置调整,但当对象旋转后,屏幕坐标系与对象的本地旋转坐标系存在角度差,导致delta的方向和实际需要的缩放方向不匹配,最终出现位置偏移。必须先将鼠标操作转换到对象的本地旋转空间,再进行计算。

修复方案

关键步骤

  1. 坐标系转换:将屏幕上的鼠标delta,逆旋转对象的角度,转换到对象的本地坐标系(即消除旋转影响,回到0度状态下的操作逻辑)
  2. 基于本地坐标系计算缩放:用转换后的本地delta计算scaleX/scaleY
  3. 重新计算位置偏移:根据本地坐标系的缩放结果,再转换回屏幕坐标系,调整对象的left/top

修改后的完整代码

void onActionHandler(dynamic details, GestureType gestureType,
    BasicObject object, ObjectControl control) {
  if (gestureType == GestureType.onPanDown) {
    final point = (details as DragDownDetails).localPosition;
    properties.set('firstPoint', point);
    // 记录当前对象的初始状态,作为计算基准
    properties.set('initialLeft', left);
    properties.set('initialTop', top);
    properties.set('initialScaleX', scaleX);
    properties.set('initialScaleY', scaleY);
    properties.set('initialAngle', angle);
  } else if (gestureType == GestureType.onPanUpdate) {
    final lastPoint = (details as DragUpdateDetails).localPosition;
    final firstPoint = properties.get<Offset>('firstPoint')!;
    final initialLeft = properties.get<double>('initialLeft')!;
    final initialTop = properties.get<double>('initialTop')!;
    final initialScaleX = properties.get<double>('initialScaleX')!;
    final initialScaleY = properties.get<double>('initialScaleY')!;
    final initialAngle = properties.get<double>('initialAngle')!;

    // 1. 计算屏幕坐标系下的鼠标偏移
    Offset screenDelta = lastPoint - firstPoint;
    // 2. 将屏幕偏移转换到对象本地坐标系(逆旋转对象角度)
    final radians = -initialAngle * pi / 180;
    final cosTheta = cos(radians);
    final sinTheta = sin(radians);
    Offset localDelta = Offset(
      screenDelta.dx * cosTheta - screenDelta.dy * sinTheta,
      screenDelta.dx * sinTheta + screenDelta.dy * cosTheta,
    );

    bool positiveHorizontalScale = true;
    bool positiveVerticalScale = true;
    final option = getControlOption(control)!;

    if (option == ControlsOptions.tl) {
      positiveHorizontalScale = false;
      positiveVerticalScale = false;
    } else if (option == ControlsOptions.mt || option == ControlsOptions.tr) {
      positiveVerticalScale = false;
    } else if (option == ControlsOptions.bl || option == ControlsOptions.ml) {
      positiveHorizontalScale = false;
    }

    // 调整本地偏移的方向,适配缩放控制逻辑
    if (!positiveHorizontalScale) {
      localDelta = Offset(-localDelta.dx, localDelta.dy);
    }
    if (!positiveVerticalScale) {
      localDelta = Offset(localDelta.dx, -localDelta.dy);
    }

    bool adjustHorizontal = false;
    bool adjustVertical = false;
    final initialScaledWidth = width * initialScaleX;
    final initialScaledHeight = height * initialScaleY;

    // 基于本地偏移计算新的缩放比例
    final sX = (initialScaledWidth + localDelta.dx) / width;
    final sY = (initialScaledHeight + localDelta.dy) / height;

    double tempScaleX = initialScaleX;
    double tempScaleY = initialScaleY;
    double tempLeft = initialLeft;
    double tempTop = initialTop;

    switch (option) {
      case ControlsOptions.tl:
      case ControlsOptions.tr:
      case ControlsOptions.bl:
      case ControlsOptions.br:
        tempScaleX = sX;
        tempScaleY = sY;
        adjustHorizontal = true;
        adjustVertical = true;
        break;
      case ControlsOptions.ml:
      case ControlsOptions.mr:
        tempScaleX = sX;
        adjustHorizontal = true;
        break;
      case ControlsOptions.mt:
      case ControlsOptions.mb:
        tempScaleY = sY;
        adjustVertical = true;
        break;
      case ControlsOptions.mtr:
        break;
    }

    // 计算本地坐标系下的位置偏移量
    Offset localPositionOffset = Offset.zero;
    if (adjustHorizontal) {
      final deltaWidth = (tempScaleX - initialScaleX) * width;
      switch (originX) {
        case OriginX.right:
          localPositionOffset = Offset(deltaWidth, localPositionOffset.dy);
          break;
        case OriginX.center:
          localPositionOffset = Offset(deltaWidth / 2, localPositionOffset.dy);
          break;
        case OriginX.left:
          // 左侧原点时,拖动左侧控制框需要反向调整位置
          if (!positiveHorizontalScale) {
            localPositionOffset = Offset(-deltaWidth, localPositionOffset.dy);
          }
          break;
      }
    }
    if (adjustVertical) {
      final deltaHeight = (tempScaleY - initialScaleY) * height;
      switch (originY) {
        case OriginY.bottom:
          localPositionOffset = Offset(localPositionOffset.dx, deltaHeight);
          break;
        case OriginY.center:
          localPositionOffset = Offset(localPositionOffset.dx, deltaHeight / 2);
          break;
        case OriginY.top:
          if (!positiveVerticalScale) {
            localPositionOffset = Offset(localPositionOffset.dx, -deltaHeight);
          }
          break;
      }
    }

    // 将本地位置偏移转换回屏幕坐标系(应用对象旋转角度)
    final screenRadians = initialAngle * pi / 180;
    final screenCos = cos(screenRadians);
    final screenSin = sin(screenRadians);
    Offset screenPositionOffset = Offset(
      localPositionOffset.dx * screenCos - localPositionOffset.dy * screenSin,
      localPositionOffset.dx * screenSin + localPositionOffset.dy * screenCos,
    );

    // 更新最终位置和缩放
    tempLeft += screenPositionOffset.dx;
    tempTop += screenPositionOffset.dy;

    left = tempLeft;
    top = tempTop;
    scaleX = tempScaleX;
    scaleY = tempScaleY;
    _controller!.render();

    // 更新记录的鼠标位置
    properties.set('firstPoint', lastPoint);
  }
}

关键修改点说明

  1. 坐标系转换:通过三角函数逆旋转鼠标delta,把屏幕上的操作转换为对象本地旋转前的操作逻辑,复用0度时的成熟缩放逻辑。
  2. 状态快照:在onPanDown时记录对象初始状态,避免拖动过程中多次计算导致的累积误差。
  3. 双方向偏移转换:先在本地坐标系计算位置偏移,再转换回屏幕坐标系,确保旋转后的位置调整符合视觉预期。
  4. 缩放精度优化:直接基于原始width/height计算新scale,避免多次乘除导致的精度损失。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 02:14:59