Files
flutterApp/lib/features/remote_control/presentation/widgets/driving_guide_overlay.dart
2026-10-20 16:45:51 +08:00

187 lines
6.0 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';
/// 主屏“行驶辅助线”覆盖层:中央绿色行进箭头 + 两侧汇聚车道线(外黄内绿)。
///
/// 从 pv 全功能 HUD 的假 AR 导向层抽离而来,可独立复用:
/// - 自带流动动画(绿色箭头由底部向远处“递进流动”,无需外部驱动);
/// - 脊柱以画面底部正中为固定顶点,随 [steering](度,右正左负,-45~45)
/// 做等曲率圆弧弯曲;[steering]=0 时为笔直隧道;
/// - 采样用连续插值,静止即严格直线(杜绝整型下标量化导致的折线抖动)。
///
/// 纯装饰层,调用方用 `IgnorePointer` 包一层即可,不影响手势。
class DrivingGuideOverlay extends StatefulWidget {
const DrivingGuideOverlay({super.key, this.steering = 0});
/// 转向角(度):驱动整条脊柱左右弯曲。
final double steering;
@override
State<DrivingGuideOverlay> createState() => _DrivingGuideOverlayState();
}
class _DrivingGuideOverlayState extends State<DrivingGuideOverlay>
with SingleTickerProviderStateMixin {
late final AnimationController _flow;
@override
void initState() {
super.initState();
_flow = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
}
@override
void dispose() {
_flow.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: AnimatedBuilder(
animation: _flow,
builder: (context, _) => CustomPaint(
size: Size.infinite,
painter: _DrivingGuidePainter(
progress: _flow.value,
steering: widget.steering,
),
),
),
);
}
}
class _DrivingGuidePainter extends CustomPainter {
_DrivingGuidePainter({required this.progress, required this.steering});
/// 0..1 循环进度,驱动绿色箭头的流动。
final double progress;
/// 转向角(度,右正左负,-45~45):驱动辅助线左右弯曲。
final double steering;
/// 近端(底部顶点)纵向比例
static const double _bottomT = 0.98;
/// 远端(收束)纵向比例
static const double _topT = 0.52;
/// 满舵时脊柱总转角(弧度):越大弯得越急;嫌弯不够调大、嫌夸张调小。
static const double _maxHeading = 0.5;
@override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final cx = w / 2;
final bottomY = h * _bottomT;
final topY = h * _topT;
// 近零死区:摇杆回中或极小漂移时强制视为直行,杜绝静态抖动
final rawTurn = (steering / 45.0).clamp(-1.0, 1.0);
final turn = rawTurn.abs() < 0.02 ? 0.0 : rawTurn;
final totalHeading = turn * _maxHeading;
// 航向积分采样脊柱中心线:底部正中出发、初始竖直向上
const steps = 36;
final ds = (bottomY - topY) / steps;
final pts = <Offset>[];
final heads = <double>[];
double x = cx, y = bottomY, th = 0;
for (var 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;
}
// 连续插值取脊柱点/切线角:避免整型下标量化导致的“折线抖动”
Offset spineAt(double f) {
final pos = (f * steps).clamp(0.0, steps.toDouble());
final i = pos.floor().clamp(0, steps - 1);
return Offset.lerp(pts[i], pts[i + 1], pos - i)!;
}
double headAt(double f) {
final pos = (f * steps).clamp(0.0, steps.toDouble());
final i = pos.floor().clamp(0, steps - 1);
return heads[i] + (heads[i + 1] - heads[i]) * (pos - i);
}
// 沿脊柱法线偏移半宽得到一条车道线(root/far 为相对 w 的半宽比例,近宽远窄汇聚)
void lane(
double rootHalf,
double farHalf,
Color color,
double widthPx,
double sign,
) {
final p = Paint()
..color = color
..strokeWidth = widthPx
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final path = Path();
const seg = 40;
for (var s = 0; s <= seg; s++) {
final f = s / seg;
final half = (rootHalf + (farHalf - rootHalf) * f) * w;
final n = Offset(math.cos(headAt(f)), math.sin(headAt(f)));
final pt = spineAt(f) + n * (half * sign);
if (s == 0) {
path.moveTo(pt.dx, pt.dy);
} else {
path.lineTo(pt.dx, pt.dy);
}
}
canvas.drawPath(path, p);
}
// 外黄内绿两侧车道线
lane(0.14, 0.05, const Color(0xCCFFC24D), 4, -1);
lane(0.14, 0.05, const Color(0xCCFFC24D), 4, 1);
lane(0.08, 0.02, const Color(0xAA22E0A0), 3, -1);
lane(0.08, 0.02, const Color(0xAA22E0A0), 3, 1);
// 中央绿色导向箭头:沿弯曲脊柱放置并随切线旋转,虚实渐隐
const n = 4;
for (var i = 0; i < n; i++) {
final phase = (progress + i / n) % 1.0; // 0=近/底 → 1=远/顶
final ay = h * (0.90 - phase * 0.34);
final f = ((bottomY - ay) / (bottomY - topY)).clamp(0.0, 1.0).toDouble();
final half = w * (0.105 - phase * 0.05);
final drop = half * 0.72;
final env = math.sin(phase * math.pi);
final arrow = Paint()
..color = const Color(
0xFF22E0A0,
).withValues(alpha: (0.12 + 0.8 * env).clamp(0.0, 0.95).toDouble())
..strokeWidth = 2.0 + 6.0 * env
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
final c = spineAt(f);
canvas.save();
canvas.translate(c.dx, c.dy);
canvas.rotate(headAt(f));
final path = Path()
..moveTo(-half, drop)
..lineTo(0, 0)
..lineTo(half, drop);
canvas.drawPath(path, arrow);
canvas.restore();
}
}
@override
bool shouldRepaint(covariant _DrivingGuidePainter oldDelegate) =>
oldDelegate.progress != progress || oldDelegate.steering != steering;
}