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