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

236 lines
7.9 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 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;
}