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

Flutter问题:旋转后调整Widget尺寸时如何保持偏移稳定?

旋转后调整Widget尺寸位置偏移的修复方案

问题描述

需要实现一个具备以下特性的灵活Widget:

  • 控制Widget位置
  • 旋转Widget
  • 从各边调整Widget尺寸

目前前两项功能已完成,但旋转后调整尺寸时,Widget位置会出现异常偏移。

演示效果:
旋转后调整尺寸偏移的演示

问题根源

  1. 旋转与定位层级错误:原代码中将Transform.rotate放在Positioned子节点,Positioned的left/top基于未旋转的Widget左上角定位,旋转后调整尺寸时,Widget边界变化会导致锚点偏移。
  2. 尺寸调整坐标未转换:调整尺寸时的手势delta是旋转后的局部坐标系数据,直接用于修改尺寸会导致方向错误,且未同步调整Widget位置以保持旋转中心稳定。
  3. 拖拽移动逻辑错误:原拖拽移动的坐标计算存在偏差,导致位置更新不准确。

修复方案

核心调整点

  1. 调换旋转与定位层级:将Transform.rotate放在Positioned外层,通过origin指定旋转中心为Widget中心点,同时计算旋转后Widget的边界盒来修正Positioned的定位坐标。
  2. 坐标转换修正:将手势delta从旋转后的局部坐标系转换为原始坐标系,调整尺寸时同步更新Widget的offset,保持旋转中心位置不变。
  3. 修复拖拽移动逻辑:重新计算拖拽时的位置增量,确保移动操作准确。

完整修复代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 08:20:27