182 lines
5.7 KiB
Dart
182 lines
5.7 KiB
Dart
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<ReverseGuideOverlay> createState() => _ReverseGuideOverlayState();
|
||
}
|
||
|
||
class _ReverseGuideOverlayState extends State<ReverseGuideOverlay>
|
||
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<Offset> pts = <Offset>[];
|
||
final List<double> heads = <double>[];
|
||
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 <double>[-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<double> 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;
|
||
}
|