Files
2026-02-28 10:11:33 +01:00

176 lines
5.3 KiB
Dart

import 'package:flutter/material.dart';
/// A navigation arrow pointer that indicates the user's location and direction of travel.
///
/// The pointer consists of:
/// - An outer semi-transparent circle representing GPS accuracy
/// - An inner triangular arrow pointing in the direction of travel/heading
/// - Optional rotation based on compass or GPS heading
class LocationPointer extends StatelessWidget {
/// The heading in degrees (0-360, where 0 = North, 90 = East)
/// If null or -1, the pointer will not rotate
final double? heading;
/// The primary color for the pointer
final Color color;
/// The size of the entire widget
final double size;
const LocationPointer({
super.key,
this.heading,
required this.color,
this.size = 40.0,
});
@override
Widget build(BuildContext context) {
// Determine if we have valid heading data
final hasValidHeading = heading != null && heading! >= 0;
// Calculate rotation angle (convert heading to radians)
final rotationAngle = hasValidHeading ? (heading! * 3.14159 / 180.0) : 0.0;
return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
// Outer accuracy circle (very subtle, uses theme color)
Container(
width: size * 0.6,
height: size * 0.6,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
),
// Inner rotatable arrow pointer (much larger - 90% of size)
Transform.rotate(
angle: rotationAngle,
child: CustomPaint(
size: Size(size * 0.9, size * 0.9),
painter: _NavigationPointerPainter(
color: color,
hasValidHeading: hasValidHeading,
),
),
),
],
),
);
}
}
/// Custom painter that draws a navigation arrow pointer
class _NavigationPointerPainter extends CustomPainter {
final Color color;
final bool hasValidHeading;
_NavigationPointerPainter({
required this.color,
required this.hasValidHeading,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final width = size.width;
final height = size.height;
if (hasValidHeading) {
// Create navigation arrow with V-shaped cutout at bottom
final arrowPath = Path();
// Top point (sharp tip)
arrowPath.moveTo(center.dx, height * 0.08);
// Right side down to bottom right
arrowPath.lineTo(center.dx + width * 0.42, height * 0.92);
// V-cutout at bottom - right side to center
arrowPath.lineTo(center.dx, height * 0.70);
// V-cutout - center to left side
arrowPath.lineTo(center.dx - width * 0.42, height * 0.92);
// Left side back up to top
arrowPath.lineTo(center.dx, height * 0.08);
arrowPath.close();
// Draw shadow for depth
final shadowPaint = Paint()
..color = Colors.black.withValues(alpha: 0.25)
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4);
canvas.save();
canvas.translate(2, 2);
canvas.drawPath(arrowPath, shadowPaint);
canvas.restore();
// Left side (lighter - 70% opacity of theme color)
final leftSidePath = Path();
leftSidePath.moveTo(center.dx, height * 0.08);
leftSidePath.lineTo(center.dx - width * 0.42, height * 0.92);
leftSidePath.lineTo(center.dx, height * 0.70);
leftSidePath.close();
final leftPaint = Paint()
..color = color.withValues(alpha: 0.7)
..style = PaintingStyle.fill;
canvas.drawPath(leftSidePath, leftPaint);
// Right side (darker - full theme color)
final rightSidePath = Path();
rightSidePath.moveTo(center.dx, height * 0.08);
rightSidePath.lineTo(center.dx, height * 0.70);
rightSidePath.lineTo(center.dx + width * 0.42, height * 0.92);
rightSidePath.close();
final rightPaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawPath(rightSidePath, rightPaint);
// Optional: Draw white border for contrast
final borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..strokeJoin = StrokeJoin.round;
canvas.drawPath(arrowPath, borderPaint);
} else {
// No heading available - draw a circle with white border (uses theme color)
final circlePaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawCircle(center, width * 0.4, circlePaint);
// White border
final borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.5;
canvas.drawCircle(center, width * 0.4, borderPaint);
// Center white dot
final centerDot = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(center, width * 0.15, centerDot);
}
}
@override
bool shouldRepaint(_NavigationPointerPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.hasValidHeading != hasValidHeading;
}
}