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 warnColor = _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); } Path railPath(double sign) { 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); } return path; } // ① 左右导轨:由“细线”改为**分段色带**(倒车影像标准标尺)—— // 远端红 → 中段黄 → 近端绿,并沿纵向做透明度渐变(远端淡出), // 再用“模糊光晕 + 实色芯”两层叠加做出边缘虚化的发光感。 // 路径仍是同一批采样点,引导线走向 / 弯曲 / 宽度轮廓完全不变。 final double bandW = math.max(5.0, w * 0.024); final Shader rulerShader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0x00FF4D4D), // 远端:红,完全淡出 Color(0xCCFF4D4D), // 红标尺 Color(0xE6FFC61A), // 黄标尺 Color(0xF235D07F), // 近端:绿,最实 ], stops: [0.0, 0.32, 0.56, 0.80], ).createShader(Rect.fromLTRB(0, h * _farT, w, bottomY)); for (final double sign in const [-1.0, 1.0]) { final Path path = railPath(sign); // 外层:宽一圈的模糊光晕(渐变虚化) canvas.drawPath( path, Paint() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..strokeWidth = bandW * 1.9 ..shader = rulerShader ..maskFilter = MaskFilter.blur(BlurStyle.normal, bandW * 0.55), ); // 内层:实色带(形状明确,不依赖模糊) canvas.drawPath( path, Paint() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..strokeWidth = bandW ..shader = rulerShader, ); } // ② 横向刻度:加粗成“横带”,越近越粗越实;两端向外渐隐 + 轻微模糊, // 保留原来沿流动虚线的动画(相位仍由 _ticker 驱动)。 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); final Offset a = rail(i, -1.0); final Offset b = rail(i, 1.0); final double f = i / steps; final double alpha = 0.30 + 0.65 * f; final Paint dashPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = math.max(3.0, w * (0.006 + 0.012 * f)) ..strokeCap = StrokeCap.round // 两端淡出:从导轨内侧向外渐隐,像参考图那样“融入画面” ..shader = LinearGradient( colors: [ warnColor.withValues(alpha: 0.0), warnColor.withValues(alpha: alpha), warnColor.withValues(alpha: alpha), warnColor.withValues(alpha: 0.0), ], stops: const [0.0, 0.24, 0.76, 1.0], ).createShader(Rect.fromPoints(a, b)) ..maskFilter = MaskFilter.blur( BlurStyle.normal, math.max(0.8, w * 0.0025), ); _drawDashedLine( canvas, a, b, 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; }