import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../domain/entities/reverse_camera_data.dart'; /// 倒车影像引导线动画层:叠在视频画面之上,仅倒车挡时显示。 /// /// 动画由两部分驱动: /// - 嵌入式回传数据([ReverseCameraData]):转向角 → 引导线绕底部顶点弯成圆弧, /// 距离 → 颜色刻度; /// - 本地 AnimationController:横向刻度线的流动虚线,营造“行进中”的动感。 class ReverseGuideOverlay extends StatefulWidget { final ReverseCameraData data; const ReverseGuideOverlay({super.key, required this.data}); @override State createState() => _ReverseGuideOverlayState(); } class _ReverseGuideOverlayState extends State with SingleTickerProviderStateMixin { late final AnimationController _ticker; @override void initState() { super.initState(); _ticker = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(); } @override void dispose() { _ticker.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!widget.data.isReversing) return const SizedBox.shrink(); return RepaintBoundary( child: AnimatedBuilder( animation: _ticker, builder: (BuildContext context, Widget? child) => CustomPaint( size: Size.infinite, painter: _GuidePainter(data: widget.data, dashPhase: _ticker.value), ), ), ); } } class _GuidePainter extends CustomPainter { final ReverseCameraData data; final double dashPhase; _GuidePainter({required this.data, required this.dashPhase}); /// 近端(画面底部)纵向位置比例 = 引导线的固定顶点(锚在车尾) static const double _nearT = 0.98; /// 远端(收束线)纵向位置比例 static const double _farT = 0.45; /// 满舵时的总转角(弧度):越大弯得越急,约 34°;嫌弯不够调大、嫌夸张调小。 static const double _maxHeading = 0.6; @override void paint(Canvas canvas, Size size) { final double w = size.width; final double h = size.height; final double bottomY = h * _nearT; final double len = bottomY - h * _farT; // 转向比例:-1 满左、+1 满右、0 直行;映射为有符号总转角(决定弯向与弯度) final double turn = (data.steeringAngle / 45.0).clamp(-1.0, 1.0); final double totalHeading = turn * _maxHeading; const int steps = 40; final double ds = len / steps; // 航向积分采样中心线:起点在底部正中、初始竖直向上,逐段累加航向 → 等曲率圆弧 final List pts = []; final List heads = []; double x = w / 2; double y = bottomY; double th = 0; for (int i = 0; i <= steps; i++) { pts.add(Offset(x, y)); heads.add(th); x += math.sin(th) * ds; y -= math.cos(th) * ds; th += totalHeading / steps; } final double rootHalf = w * 0.16; final double farHalf = w * 0.30; final Color color = _colorByDistance(data.rearDistanceCm); // 中心线沿法线各偏移半宽 = 左右导轨(近窄远宽,转弯时通道宽度守恒) Offset rail(int i, double sign) { final double f = i / steps; final double half = rootHalf + (farHalf - rootHalf) * f; final Offset normal = Offset(math.cos(heads[i]), math.sin(heads[i])); return pts[i] + normal * (half * sign); } final Paint sidePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = math.max(3.0, w * 0.006) ..strokeCap = StrokeCap.round ..color = color ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2); for (final double sign in const [-1.0, 1.0]) { final Offset p0 = rail(0, sign); final Path path = Path()..moveTo(p0.dx, p0.dy); for (int i = 1; i <= steps; i++) { final Offset r = rail(i, sign); path.lineTo(r.dx, r.dy); } canvas.drawPath(path, sidePaint); } final Paint dashPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = math.max(2.0, w * 0.004) ..color = color.withValues(alpha: 0.9); // 横向刻度线:沿法线连接左右导轨;每条错开相位,虚线像“往外流动” const List stops = [0.0, 0.34, 0.63, 0.86]; for (int k = 0; k < stops.length; k++) { final int i = (stops[k] * steps).round().clamp(0, steps); _drawDashedLine( canvas, rail(i, -1.0), rail(i, 1.0), dashPaint, (dashPhase + k * 0.25) % 1.0, ); } } void _drawDashedLine( Canvas canvas, Offset a, Offset b, Paint paint, double phase, ) { final double len = (b - a).distance; if (len <= 0) return; const double dash = 18.0; const double gap = 14.0; final Offset dir = (b - a) / len; double pos = phase * (dash + gap); while (pos < len) { final double segEnd = math.min(pos + dash, len); canvas.drawLine(a + dir * pos, a + dir * segEnd, paint); pos += dash + gap; } } Color _colorByDistance(int? cm) { if (cm == null) return const Color(0xFF35D07F); // 无测距默认绿 if (cm < 80) return const Color(0xFFFF4D4D); // 红:危险 if (cm < 150) return const Color(0xFFFFC61A); // 黄:警告 return const Color(0xFF35D07F); // 绿:安全 } @override bool shouldRepaint(covariant _GuidePainter oldDelegate) => oldDelegate.data.steeringAngle != data.steeringAngle || oldDelegate.data.rearDistanceCm != data.rearDistanceCm || oldDelegate.data.gear != data.gear || oldDelegate.dashPhase != dashPhase; }