Flutter Canvas库旋转对象缩放位置偏移问题求助
问题解决:旋转后Canvas对象缩放位置偏移修复
核心问题根源
你的代码直接使用**屏幕坐标系的鼠标偏移量(delta)**计算缩放和位置调整,但当对象旋转后,屏幕坐标系与对象的本地旋转坐标系存在角度差,导致delta的方向和实际需要的缩放方向不匹配,最终出现位置偏移。必须先将鼠标操作转换到对象的本地旋转空间,再进行计算。
修复方案
关键步骤
- 坐标系转换:将屏幕上的鼠标delta,逆旋转对象的角度,转换到对象的本地坐标系(即消除旋转影响,回到0度状态下的操作逻辑)
- 基于本地坐标系计算缩放:用转换后的本地delta计算scaleX/scaleY
- 重新计算位置偏移:根据本地坐标系的缩放结果,再转换回屏幕坐标系,调整对象的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); } }
关键修改点说明
- 坐标系转换:通过三角函数逆旋转鼠标delta,把屏幕上的操作转换为对象本地旋转前的操作逻辑,复用0度时的成熟缩放逻辑。
- 状态快照:在
onPanDown时记录对象初始状态,避免拖动过程中多次计算导致的累积误差。 - 双方向偏移转换:先在本地坐标系计算位置偏移,再转换回屏幕坐标系,确保旋转后的位置调整符合视觉预期。
- 缩放精度优化:直接基于原始width/height计算新scale,避免多次乘除导致的精度损失。
内容的提问来源于stack exchange,提问作者anonymouscoder
相关产品推荐
相关产品推荐

