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/follow_drone_provider.dart';
|
||||||
import '../providers/map_controller_provider.dart';
|
import '../providers/map_controller_provider.dart';
|
||||||
import '../providers/telemetry_provider.dart';
|
import '../providers/telemetry_provider.dart';
|
||||||
|
import 'drone_follow_icon.dart';
|
||||||
import 'nav_icon_button.dart';
|
import 'nav_icon_button.dart';
|
||||||
|
|
||||||
/// Kartensteuerung im Fly-Modus (Doku 3.11): Ortssuche/Home entfallen hier
|
/// Kartensteuerung im Fly-Modus (Doku 3.11): Ortssuche/Home entfallen hier
|
||||||
@@ -16,10 +17,11 @@ import 'nav_icon_button.dart';
|
|||||||
class FlyMapControls extends ConsumerWidget {
|
class FlyMapControls extends ConsumerWidget {
|
||||||
const FlyMapControls({super.key});
|
const FlyMapControls({super.key});
|
||||||
|
|
||||||
/// Zielzoom beim Aktivieren des Folgemodus - naeher als der
|
/// Zielzoom beim Aktivieren des Folgemodus - deutlich weiter herausgezoomt
|
||||||
/// Standard-Kartenzoom (siehe MissionMap._initialZoom), damit der Sprung
|
/// als der Standard-Kartenzoom (siehe MissionMap._initialZoom), damit
|
||||||
/// tatsaechlich als Zoom wahrgenommen wird und nicht nur als Verschieben.
|
/// waehrend des Verfolgens genug Gelaende/Route um die Drohne herum
|
||||||
static const _followZoom = 18.0;
|
/// sichtbar bleibt statt nur ihrer unmittelbaren Umgebung.
|
||||||
|
static const _followZoom = 16.0;
|
||||||
|
|
||||||
/// Erster Tap zoomt auf die aktuelle Telemetrieposition und aktiviert den
|
/// Erster Tap zoomt auf die aktuelle Telemetrieposition und aktiviert den
|
||||||
/// Folgemodus (FlyScreen haengt sich darauf ein, zieht die Karte bei
|
/// Folgemodus (FlyScreen haengt sich darauf ein, zieht die Karte bei
|
||||||
@@ -41,6 +43,14 @@ class FlyMapControls extends ConsumerWidget {
|
|||||||
ref.read(followDroneProvider.notifier).set(true);
|
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) {
|
void _onFitPressed(BuildContext context, WidgetRef ref) {
|
||||||
final waypoints = ref.read(currentMissionProvider);
|
final waypoints = ref.read(currentMissionProvider);
|
||||||
if (waypoints.isEmpty) {
|
if (waypoints.isEmpty) {
|
||||||
@@ -48,6 +58,7 @@ class FlyMapControls extends ConsumerWidget {
|
|||||||
.showSnackBar(const SnackBar(content: Text('No waypoints to fit')));
|
.showSnackBar(const SnackBar(content: Text('No waypoints to fit')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ref.read(followDroneProvider.notifier).set(false);
|
||||||
fitMapToWaypoints(ref, waypoints);
|
fitMapToWaypoints(ref, waypoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +69,7 @@ class FlyMapControls extends ConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
NavIconButton(
|
NavIconButton(
|
||||||
icon: following ? Icons.gps_fixed : Icons.my_location,
|
iconWidget: DroneFollowIcon(active: following),
|
||||||
active: following,
|
|
||||||
onTap: () => _onDronePressed(context, ref),
|
onTap: () => _onDronePressed(context, ref),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ import 'package:flutter/material.dart';
|
|||||||
class NavIconButton extends StatelessWidget {
|
class NavIconButton extends StatelessWidget {
|
||||||
const NavIconButton({
|
const NavIconButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
this.icon,
|
||||||
|
this.iconWidget,
|
||||||
required this.onTap,
|
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;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -28,11 +33,8 @@ class NavIconButton extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Colors.black.withValues(alpha: 0.55),
|
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/sharing_provider.dart';
|
||||||
import 'package:dmc_app/ui/providers/telemetry_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/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/full_value_chart.dart';
|
||||||
import 'package:dmc_app/ui/widgets/halo_menu.dart';
|
import 'package:dmc_app/ui/widgets/halo_menu.dart';
|
||||||
import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
|
import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
|
||||||
@@ -984,13 +985,13 @@ void main() {
|
|||||||
|
|
||||||
// Noch keine Telemetrie eingetroffen (MockFlightControllerLink tickt
|
// Noch keine Telemetrie eingetroffen (MockFlightControllerLink tickt
|
||||||
// erst nach 1s) - Hinweis statt Kamerasprung.
|
// erst nach 1s) - Hinweis statt Kamerasprung.
|
||||||
await tester.tap(find.byIcon(Icons.my_location));
|
await tester.tap(find.byType(DroneFollowIcon));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('No telemetry yet'), findsOneWidget);
|
expect(find.text('No telemetry yet'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pump(const Duration(seconds: 1, milliseconds: 100));
|
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();
|
await tester.pump();
|
||||||
|
|
||||||
final camera = container.read(mapControllerProvider).camera;
|
final camera = container.read(mapControllerProvider).camera;
|
||||||
|
|||||||
Reference in New Issue
Block a user