follow mode button, zoom mode and abort behavior tuning

This commit is contained in:
Constantin Leue
2026-08-03 22:29:22 +02:00
parent 2f4353f3fa
commit ba14af8469
4 changed files with 108 additions and 20 deletions
+75
View File
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
/// Statisches (nicht rotierendes) Papierflieger-Symbol fuer den
/// Kartenfolge-Knopf in der Kopfleiste (FlyMapControls) - dieselbe
/// Silhouette wie [DroneMarkerIcon] auf der Karte, hier aber einfarbig
/// statt zweifarbig: nur Umriss im inaktiven Zustand, komplett mit dem
/// Akzent-Pink gefuellt im aktiven Folgemodus.
class DroneFollowIcon extends StatelessWidget {
const DroneFollowIcon({super.key, this.size = 16, this.active = false});
final double size;
final bool active;
@override
Widget build(BuildContext context) {
// NavIconButton zwingt seinem Kind straffe 34x34-Constraints auf - ohne
// Center/SizedBox wuerde CustomPaint sein size-Argument ignorieren und
// auf die volle Knopfgroesse aufgeblasen (RenderCustomPaint hat anders
// als Icon kein eingebautes Center, das die Constraints fuer sein Kind
// lockert). Center() lockert die Constraints, die SizedBox erzwingt
// danach die tatsaechlich gewuenschte, kleinere Zeichenflaeche.
return Center(
child: SizedBox(
width: size,
height: size,
child: CustomPaint(painter: _DroneFollowPainter(active: active)),
),
);
}
}
class _DroneFollowPainter extends CustomPainter {
_DroneFollowPainter({required this.active});
final bool active;
// Gleiche Koordinaten wie DroneMarkerIcon._PaperPlanePainter (viewBox
// 0 0 200 200, aus design/B_paperplane.svg), hier als ein einzelner
// Umriss statt zweier separat gefuellter Haelften.
static const _viewBoxSize = 200.0;
static const _nose = Offset(100, 22);
static const _rightWing = Offset(164, 172);
static const _tailNotch = Offset(100, 138);
static const _leftWing = Offset(36, 172);
static const _pink = Color(0xFFFF2E93);
static const _strokeWidth = 16.0;
@override
void paint(Canvas canvas, Size size) {
final scale = size.width / _viewBoxSize;
Offset s(Offset p) => p * scale;
final outline = Path()
..moveTo(s(_nose).dx, s(_nose).dy)
..lineTo(s(_rightWing).dx, s(_rightWing).dy)
..lineTo(s(_tailNotch).dx, s(_tailNotch).dy)
..lineTo(s(_leftWing).dx, s(_leftWing).dy)
..close();
canvas.drawPath(
outline,
active
? (Paint()..color = _pink)
: (Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = _strokeWidth * scale
..strokeJoin = StrokeJoin.round),
);
}
@override
bool shouldRepaint(covariant _DroneFollowPainter oldDelegate) =>
oldDelegate.active != active;
}
+16 -6
View File
@@ -6,6 +6,7 @@ import '../providers/current_mission_provider.dart';
import '../providers/follow_drone_provider.dart';
import '../providers/map_controller_provider.dart';
import '../providers/telemetry_provider.dart';
import 'drone_follow_icon.dart';
import 'nav_icon_button.dart';
/// Kartensteuerung im Fly-Modus (Doku 3.11): Ortssuche/Home entfallen hier
@@ -16,10 +17,11 @@ import 'nav_icon_button.dart';
class FlyMapControls extends ConsumerWidget {
const FlyMapControls({super.key});
/// Zielzoom beim Aktivieren des Folgemodus - naeher als der
/// Standard-Kartenzoom (siehe MissionMap._initialZoom), damit der Sprung
/// tatsaechlich als Zoom wahrgenommen wird und nicht nur als Verschieben.
static const _followZoom = 18.0;
/// Zielzoom beim Aktivieren des Folgemodus - deutlich weiter herausgezoomt
/// als der Standard-Kartenzoom (siehe MissionMap._initialZoom), damit
/// waehrend des Verfolgens genug Gelaende/Route um die Drohne herum
/// sichtbar bleibt statt nur ihrer unmittelbaren Umgebung.
static const _followZoom = 16.0;
/// Erster Tap zoomt auf die aktuelle Telemetrieposition und aktiviert den
/// Folgemodus (FlyScreen haengt sich darauf ein, zieht die Karte bei
@@ -41,6 +43,14 @@ class FlyMapControls extends ConsumerWidget {
ref.read(followDroneProvider.notifier).set(true);
}
/// Beendet den Folgemodus explizit, statt sich auf den Abbruch bei
/// manueller Karteninteraktion in FlyScreen zu verlassen (FlyScreen._
/// onMapEvent) - fitMapToWaypoints() bewegt die Kamera ueber denselben
/// MapController (source == mapController), gilt dort also nicht als
/// manuelle Geste und wuerde den Folgemodus sonst aktiv lassen. Ohne
/// diesen Abbruch wuerde der naechste Telemetrie-Frame die Kamera sofort
/// wieder von der gerade eingestellten Missionsuebersicht weg auf die
/// Drohne zurueckziehen.
void _onFitPressed(BuildContext context, WidgetRef ref) {
final waypoints = ref.read(currentMissionProvider);
if (waypoints.isEmpty) {
@@ -48,6 +58,7 @@ class FlyMapControls extends ConsumerWidget {
.showSnackBar(const SnackBar(content: Text('No waypoints to fit')));
return;
}
ref.read(followDroneProvider.notifier).set(false);
fitMapToWaypoints(ref, waypoints);
}
@@ -58,8 +69,7 @@ class FlyMapControls extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
NavIconButton(
icon: following ? Icons.gps_fixed : Icons.my_location,
active: following,
iconWidget: DroneFollowIcon(active: following),
onTap: () => _onDronePressed(context, ref),
),
const SizedBox(width: 8),
+14 -12
View File
@@ -6,18 +6,23 @@ import 'package:flutter/material.dart';
class NavIconButton extends StatelessWidget {
const NavIconButton({
super.key,
required this.icon,
this.icon,
this.iconWidget,
required this.onTap,
this.active = false,
});
}) : assert(
icon != null || iconWidget != null,
'Entweder icon oder iconWidget angeben.',
);
final IconData? icon;
/// Individuelles Icon-Widget statt eines Material-[IconData], z.B.
/// [DroneFollowIcon] fuer den Kartenfolge-Knopf (FlyMapControls). Hat
/// Vorrang vor [icon], falls beides gesetzt ist.
final Widget? iconWidget;
final IconData icon;
final VoidCallback onTap;
/// Rosa Akzentrand (Doku 3.10: DroneMarkerIcon-Farbe) fuer Knoepfe mit
/// an/aus-Zustand, z.B. den Drohnen-Folgemodus (FlyMapControls).
final bool active;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -28,11 +33,8 @@ class NavIconButton extends StatelessWidget {
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withValues(alpha: 0.55),
border: active
? Border.all(color: const Color(0xFFFF2E93), width: 2)
: null,
),
child: Icon(icon, color: Colors.white, size: 16),
child: iconWidget ?? Icon(icon, color: Colors.white, size: 16),
),
);
}