Flutter问题:旋转后调整Widget尺寸时如何保持偏移稳定?
旋转后调整Widget尺寸位置偏移的修复方案
问题描述
需要实现一个具备以下特性的灵活Widget:
- 控制Widget位置
- 旋转Widget
- 从各边调整Widget尺寸
目前前两项功能已完成,但旋转后调整尺寸时,Widget位置会出现异常偏移。
演示效果:
问题根源
- 旋转与定位层级错误:原代码中将
Transform.rotate放在Positioned子节点,Positioned的left/top基于未旋转的Widget左上角定位,旋转后调整尺寸时,Widget边界变化会导致锚点偏移。 - 尺寸调整坐标未转换:调整尺寸时的手势
delta是旋转后的局部坐标系数据,直接用于修改尺寸会导致方向错误,且未同步调整Widget位置以保持旋转中心稳定。 - 拖拽移动逻辑错误:原拖拽移动的坐标计算存在偏差,导致位置更新不准确。
修复方案
核心调整点
- 调换旋转与定位层级:将
Transform.rotate放在Positioned外层,通过origin指定旋转中心为Widget中心点,同时计算旋转后Widget的边界盒来修正Positioned的定位坐标。 - 坐标转换修正:将手势
delta从旋转后的局部坐标系转换为原始坐标系,调整尺寸时同步更新Widget的offset,保持旋转中心位置不变。 - 修复拖拽移动逻辑:重新计算拖拽时的位置增量,确保移动操作准确。
完整修复代码
import 'package:flutter/material.dart'; import 'dart:math'; void main() { return runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, home: const TestingDesign2(), ); } } class Item { Size size; Offset offset; double rotation; final GlobalKey parentKey; Item({required this.parentKey, required this.size, required this.offset, required this.rotation}); } class TestingDesign2 extends StatefulWidget { const TestingDesign2({super.key}); @override State<TestingDesign2> createState() => _TestingDesign2State(); } class _TestingDesign2State extends State<TestingDesign2> { final Item item = Item( parentKey: GlobalKey(), offset: const Offset(10, 10), size: const Size(150, 150), rotation: 0, ); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Testing design 2")), body: Container( color: Colors.blueAccent.withOpacity(0.5), child: Stack( children: [ItemTesting(key: item.parentKey, item: item)], ), ), ); } } class ItemTesting extends StatefulWidget { final Item item; ItemTesting({super.key, required this.item}); @override State<ItemTesting> createState() => _ItemTestingState(); } class _ItemTestingState extends State<ItemTesting> { double offsetAngle = 0; bool isRotate = false; Offset touchPosition = Offset.zero; @override Widget build(BuildContext context) { // 计算旋转后Widget的边界盒,确定Positioned的起始坐标 final center = Offset( widget.item.offset.dx + widget.item.size.width / 2, widget.item.offset.dy + widget.item.size.height / 2, ); final halfSize = Offset(widget.item.size.width / 2, widget.item.size.height / 2); final corners = [ -halfSize, Offset(halfSize.dx, -halfSize.dy), Offset(-halfSize.dx, halfSize.dy), halfSize, ].map((corner) => _rotatePoint(corner, widget.item.rotation) + center).toList(); final minLeft = corners.map((p) => p.dx).reduce((a, b) => a < b ? a : b); final minTop = corners.map((p) => p.dy).reduce((a, b) => a < b ? a : b); return Positioned( left: minLeft, top: minTop, child: Transform.rotate( angle: widget.item.rotation, origin: halfSize, child: SizedBox( height: widget.item.size.height, width: widget.item.size.width, child: Stack( children: [ GestureDetector( onPanStart: onPanStart, onPanUpdate: onPanUpdate, behavior: HitTestBehavior.translucent, onPanEnd: (_) => isRotate = false, onPanCancel: () => isRotate = false, child: Container(color: Colors.red), ), const IgnorePointer( child: Align( alignment: Alignment.topRight, child: ClipOval( child: SizedBox( height: 25, width: 25, child: ColoredBox( color: Colors.blue, child: Icon(Icons.rotate_right_outlined, size: 18), ), ), ), ), ), Align( alignment: Alignment.centerRight, child: GestureDetector( onPanUpdate: resizeRight, child: const SizedBox( height: 25, width: 25, child: ColoredBox( color: Colors.yellow, child: Icon(Icons.arrow_forward, size: 18), ), ), ), ), Align( alignment: Alignment.bottomCenter, child: GestureDetector( onPanUpdate: resizeBottom, child: const SizedBox( height: 25, width: 25, child: ColoredBox( color: Colors.yellow, child: Icon(Icons.arrow_downward, size: 18), ), ), ), ), ], ), ), ), ); } // 点坐标旋转转换 Offset _rotatePoint(Offset point, double angle) { final cosAngle = cos(angle); final sinAngle = sin(angle); return Offset( point.dx * cosAngle - point.dy * sinAngle, point.dx * sinAngle + point.dy * cosAngle, ); } void onPanStart(DragStartDetails details) { final center = Offset(widget.item.size.width / 2, widget.item.size.height / 2); final touchFromCenter = details.localPosition - center; offsetAngle = touchFromCenter.direction - widget.item.rotation; final RenderBox referenceBox = widget.item.parentKey.currentContext!.findRenderObject() as RenderBox; touchPosition = referenceBox.globalToLocal(details.globalPosition); // 判断是否触发旋转操作 if (details.localPosition.dx > widget.item.size.width - 25 && details.localPosition.dy <= 25) { isRotate = true; } } void onPanUpdate(DragUpdateDetails details) { if (isRotate) { final center = Offset(widget.item.size.width / 2, widget.item.size.height / 2); final touchFromCenter = details.localPosition - center; widget.item.rotation = touchFromCenter.direction - offsetAngle; } else { final RenderBox referenceBox = widget.item.parentKey.currentContext!.findRenderObject() as RenderBox; final currentTouchPos = referenceBox.globalToLocal(details.globalPosition); widget.item.offset += currentTouchPos - touchPosition; touchPosition = currentTouchPos; } setState(() {}); } void resizeRight(DragUpdateDetails details) { setState(() { // 将旋转后的delta转换为原始坐标系增量 final delta = _rotatePoint(details.delta, -widget.item.rotation); final newWidth = max(50, widget.item.size.width + delta.dx); // 调整offset保持中心位置不变 widget.item.offset += Offset((newWidth - widget.item.size.width) / 2, 0); widget.item.size = Size(newWidth, widget.item.size.height); }); } void resizeBottom(DragUpdateDetails details) { setState(() { // 将旋转后的delta转换为原始坐标系增量 final delta = _rotatePoint(details.delta, -widget.item.rotation); final newHeight = max(50, widget.item.size.height + delta.dy); // 调整offset保持中心位置不变 widget.item.offset += Offset(0, (newHeight - widget.item.size.height) / 2); widget.item.size = Size(widget.item.size.width, newHeight); }); } }
内容的提问来源于stack exchange,提问作者mamena tech
相关产品推荐
相关产品推荐

