187 lines
6.0 KiB
Dart
187 lines
6.0 KiB
Dart
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;
|
||
}
|