Files
flutterApp/lib/features/remote_control/presentation/widgets/reverse_guide_overlay.dart

182 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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