Flutter旋转线条atan2角度计算:90度偏移问题
解决Flutter旋转线条的90度偏移问题
问题根源
你在_updateAngle函数中错误地对计算出的角度添加了90度偏移,导致触摸位置对应的角度与线条实际绘制的角度完全错位;同时角度调整时机错误(在约束端点到圆周之后才调整),进一步加剧了偏移问题。
修正方案
移除多余的90度偏移,确保角度计算与线条绘制逻辑一致:
修改后的完整代码
import 'package:flutter/material.dart'; import 'dart:math'; import 'package:vibration/vibration.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primaryColor: Colors.black, useMaterial3: true, ), home: const MyHomePage(title: 'Angle Inputter'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { double _angle = 0.0; bool _dotPressed = false; bool _isPointInsideDot(Offset point, Offset dotCenter, double radius) { return (point - dotCenter).distance <= radius; } Rect _getDotTouchTarget(Offset dotCenter, double dotRadius) { const double touchTargetPadding = 24.0; return Rect.fromCenter( center: dotCenter, width: 2 * (dotRadius + touchTargetPadding), height: 2 * (dotRadius + touchTargetPadding), ); } void _updateAngle(Offset position) { final Offset center = Offset(100, 100); // 圆心坐标 final double radius = 100; // 圆半径 // 计算触摸点相对于圆心的角度(已适配Flutter坐标系) double newAngle = (atan2(position.dy - center.dy, position.dx - center.dx) * 180 / pi) % 360; // 约束线条端点在圆周上(使用正确角度计算) final double dx = radius * cos(newAngle * pi / 180); final double dy = radius * sin(newAngle * pi / 180); final Offset constrainedPosition = center + Offset(dx, dy); setState(() { _angle = newAngle; }); } @override Widget build(BuildContext context) { double displayAngle = _angle <= 180 ? _angle : 360 - _angle; return Scaffold( appBar: AppBar( backgroundColor: Colors.black, foregroundColor: Colors.white, title: Text(widget.title), ), body: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Angle: ${displayAngle.toInt()}°', style: Theme.of(context).textTheme.titleLarge, ), ), Expanded( child: Center( child: SizedBox( width: 200, height: 200, child: GestureDetector( onPanStart: (details) { final dotRadius = _dotPressed ? 12.0 : 6.0; final Offset center = Offset(100, 100); final Offset dotCenter = Offset( center.dx + 100 * cos(_angle * pi / 180), center.dy + 100 * sin(_angle * pi / 180), ); if (_getDotTouchTarget(dotCenter, dotRadius).contains(details.localPosition)) { setState(() { _dotPressed = true; Vibration.vibrate(); }); } }, onPanUpdate: (details) { if (_dotPressed) { _updateAngle(details.localPosition); } }, onPanEnd: (details) { setState(() { _dotPressed = false; }); }, child: CustomPaint( size: const Size(200, 200), painter: AnglePainter(_angle, _dotPressed), ), ), ), ), ), ], ), ); } } class AnglePainter extends CustomPainter { final double angle; final bool dotPressed; AnglePainter(this.angle, this.dotPressed); @override void paint(Canvas canvas, Size size) { final double radius = size.width / 2; final Offset center = Offset(size.width / 2, size.height / 2); // 绘制蓝色外圆 final circlePaint = Paint() ..color = Colors.blue ..strokeWidth = 4.0 ..style = PaintingStyle.stroke; canvas.drawCircle(center, radius, circlePaint); // 绘制绿色参考线(向右) final referenceLinePaint = Paint() ..color = Colors.green ..strokeWidth = 2.0; canvas.drawLine(center, Offset(center.dx + radius, center.dy), referenceLinePaint); // 计算红色旋转线条的端点 final double radian = angle * (pi / 180); final Offset endPoint = Offset( center.dx + radius * cos(radian), center.dy + radius * sin(radian), ); // 绘制红色旋转线条 final linePaint = Paint() ..color = Colors.red ..strokeWidth = 4.0; canvas.drawLine(center, endPoint, linePaint); // 绘制线条末端的蓝色圆点 final dotPaint = Paint() ..color = Colors.blue ..style = PaintingStyle.fill; canvas.drawCircle(endPoint, dotPressed ? 12.0 : 6.0, dotPaint); // 绘制角度文本 final textSpan = TextSpan( text: '${(angle <= 180 ? angle : 360 - angle).toStringAsFixed(1)}°', style: const TextStyle(color: Colors.black, fontSize: 18), ); final textPainter = TextPainter( text: textSpan, textDirection: TextDirection.ltr, ); textPainter.layout( minWidth: 0, maxWidth: size.width, ); final textOffset = Offset(center.dx - textPainter.width / 2, center.dy - radius - textPainter.height - 10); textPainter.paint(canvas, textOffset); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } }
关键改动说明
- 移除多余的90度偏移:删除
newAngle = (newAngle + 90) % 360;这一行,atan2(position.dy - center.dy, position.dx - center.dx)已经直接计算出触摸点相对于圆心的正确角度,与线条绘制逻辑(cos(angle)、sin(angle))完全匹配。 - 保持逻辑一致性:使用未偏移的角度计算约束后的端点位置,确保触摸位置与线条端点始终对应,解决拖动时的跳转问题。
额外优化建议
- 将
center和radius定义为类常量,避免在函数中重复创建; - 把角度归一化逻辑(
displayAngle)封装为单独函数,提升代码可读性。
内容的提问来源于stack exchange,提问作者Akki
相关产品推荐
相关产品推荐

