follow mode button, zoom mode and abort behavior tuning
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:dmc_app/ui/providers/mission_meta_provider.dart';
|
||||
import 'package:dmc_app/ui/providers/sharing_provider.dart';
|
||||
import 'package:dmc_app/ui/providers/telemetry_provider.dart';
|
||||
import 'package:dmc_app/transport/mock/mock_flight_controller_link.dart';
|
||||
import 'package:dmc_app/ui/widgets/drone_follow_icon.dart';
|
||||
import 'package:dmc_app/ui/widgets/full_value_chart.dart';
|
||||
import 'package:dmc_app/ui/widgets/halo_menu.dart';
|
||||
import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
|
||||
@@ -984,13 +985,13 @@ void main() {
|
||||
|
||||
// Noch keine Telemetrie eingetroffen (MockFlightControllerLink tickt
|
||||
// erst nach 1s) - Hinweis statt Kamerasprung.
|
||||
await tester.tap(find.byIcon(Icons.my_location));
|
||||
await tester.tap(find.byType(DroneFollowIcon));
|
||||
await tester.pump();
|
||||
expect(find.text('No telemetry yet'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1, milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byIcon(Icons.my_location));
|
||||
await tester.tap(find.byType(DroneFollowIcon));
|
||||
await tester.pump();
|
||||
|
||||
final camera = container.read(mapControllerProvider).camera;
|
||||
|
||||
Reference in New Issue
Block a user