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

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;
  }
}

关键改动说明

  1. 移除多余的90度偏移:删除newAngle = (newAngle + 90) % 360;这一行,atan2(position.dy - center.dy, position.dx - center.dx)已经直接计算出触摸点相对于圆心的正确角度,与线条绘制逻辑(cos(angle)、sin(angle))完全匹配。
  2. 保持逻辑一致性:使用未偏移的角度计算约束后的端点位置,确保触摸位置与线条端点始终对应,解决拖动时的跳转问题。

额外优化建议

  • 将center和radius定义为类常量,避免在函数中重复创建;
  • 把角度归一化逻辑(displayAngle)封装为单独函数,提升代码可读性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 19:08:10