mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-07-02 09:31:36 +00:00
baf49d27d8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
176 lines
5.3 KiB
Dart
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;
|
|
}
|
|
}
|