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

236 lines
7.9 KiB
Dart
Raw Normal View History

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 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>[
Color(0x00FF4D4D), // 远端:红,完全淡出
Color(0xCCFF4D4D), // 红标尺
Color(0xE6FFC61A), // 黄标尺
Color(0xF235D07F), // 近端:绿,最实
],
stops: <double>[0.0, 0.32, 0.56, 0.80],
).createShader(Rect.fromLTRB(0, h * _farT, w, bottomY));
for (final double sign in const <double>[-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<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);
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: <Color>[
warnColor.withValues(alpha: 0.0),
warnColor.withValues(alpha: alpha),
warnColor.withValues(alpha: alpha),
warnColor.withValues(alpha: 0.0),
],
stops: const <double>[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;
}