Add swipe gestures to drone profile editor (vertical=big, horizontal=small step)
Ersetzt die einfachen Zahlen-TextFormFields im Drohnenprofil-Editor durch tastaturlose Swipe-Chips (Doku 3.5, HTML-Demonstrator: SwipeValueInput) - vertikales Ziehen aendert den Wert in grossen Schritten (stepBig), horizontales Ziehen in kleinen Schritten (stepSmall). Die Zugrichtung wird beim ersten Ueberschreiten einer 8px-Schwelle einmalig festgelegt und bleibt fuer den Rest der Geste gesperrt, damit eine leicht schraege Geste nicht zwischen den beiden Schrittweiten hin- und herspringt. Min/Max/ Schrittweiten je Feld 1:1 aus DRONE_EDITOR_GROUPS im HTML-Demonstrator uebernommen. Zusaetzlich ein "modified"-Punkt neben dem Label, sobald ein Wert vom Ausgangswert beim Oeffnen des Editors abweicht (HTML: .modified- dot/.chip-modified). lib/ui/widgets/swipe_value_input.dart: neuer, wiederverwendbarer Chip. Gesten-Konflikt mit der scrollbaren Formular-Liste (wichtigster Teil dieser Aenderung): Die Chips sitzen in einer vertikal scrollbaren ListView (elf Felder in Gruppen passen nicht auf einen Bildschirm). Ein GestureDetector.onPan* auf dem Chip verlor dabei auf einem echten Geraet durchgehend gegen die eigene Scroll-Geste der ListView - onPanStart/ onPanUpdate feuerten nie, weder fuer vertikale noch horizontale Zuege (mit Debug-Prints auf dem Pixel_10a-Emulator verifiziert). Behoben durch: - SwipeValueInput nutzt jetzt rohe Pointer-Events (Listener statt GestureDetector.onPan*) - diese werden unabhaengig vom Gesture-Arena- Ausgang immer zugestellt. - Ein neuer onDragActiveChanged-Callback informiert das Elternwidget, solange eine Geste aktiv ist; DroneProfileEditor sperrt darueber die ListView (NeverScrollableScrollPhysics) fuer die Dauer des Ziehens, so dass die Liste waehrenddessen nicht mitscrollt. Getestet: 7 Widget-Tests fuer SwipeValueInput (grosse/kleine Schritte, Richtungswechsel, Klemmen auf min/max, Rundung auf Nachkommastellen, "modified"-Punkt, sowie ein Regressionstest fuer exakt dieses Szenario - Chip innerhalb einer scrollbaren ListView). Alle 117 Tests sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert: vertikaler Zug auf "Min (stall)" aendert den Wert in 10er-Schritten ohne die Liste zu scrollen, horizontaler Zug auf "Cruise" in 1er-Schritten - beide mit korrekt aufleuchtendem "modified"-Indikator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c5a8c68a8e
commit
f25c312697
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:dmc_app/ui/widgets/swipe_value_input.dart';
|
||||
|
||||
void main() {
|
||||
Widget wrap({
|
||||
required double value,
|
||||
required double initialValue,
|
||||
required void Function(double) onChanged,
|
||||
double min = 0,
|
||||
double max = 200,
|
||||
double stepBig = 10,
|
||||
double stepSmall = 1,
|
||||
int decimals = 0,
|
||||
}) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: SwipeValueInput(
|
||||
label: 'Test',
|
||||
value: value,
|
||||
initialValue: initialValue,
|
||||
min: min,
|
||||
max: max,
|
||||
stepBig: stepBig,
|
||||
stepSmall: stepSmall,
|
||||
decimals: decimals,
|
||||
unit: 'm',
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('vertikales Ziehen aendert den Wert in grossen Schritten (stepBig)',
|
||||
(tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
|
||||
|
||||
// 3 Schritte a 14px nach oben -> +3*stepBig(10) = +30.
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -42));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes, isNotEmpty);
|
||||
expect(changes.last, 80);
|
||||
});
|
||||
|
||||
testWidgets('horizontales Ziehen aendert den Wert in kleinen Schritten (stepSmall)',
|
||||
(tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
|
||||
|
||||
// 3 Schritte a 14px nach rechts -> +3*stepSmall(1) = +3.
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(42, 0));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes, isNotEmpty);
|
||||
expect(changes.last, 53);
|
||||
});
|
||||
|
||||
testWidgets('nach links/unten ziehen verringert den Wert', (tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
|
||||
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(0, 42));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes.last, 20);
|
||||
});
|
||||
|
||||
testWidgets('klemmt auf max, wenn die grosse Schrittweite darueber hinausschiesst',
|
||||
(tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(wrap(value: 190, initialValue: 190, onChanged: changes.add));
|
||||
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -140));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes.last, 200);
|
||||
});
|
||||
|
||||
testWidgets('rundet auf die konfigurierte Nachkommastellenzahl', (tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(wrap(
|
||||
value: 1.0,
|
||||
initialValue: 1.0,
|
||||
stepSmall: 0.5,
|
||||
decimals: 1,
|
||||
onChanged: changes.add,
|
||||
));
|
||||
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(14, 0));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes.last, 1.5);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'aendert den Wert auch innerhalb einer scrollbaren Liste (Regression: '
|
||||
'GestureDetector.onPan* verlor frueher gegen die Scroll-Geste der Liste)',
|
||||
(tester) async {
|
||||
final changes = <double>[];
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 400),
|
||||
SwipeValueInput(
|
||||
label: 'Test',
|
||||
value: 50,
|
||||
initialValue: 50,
|
||||
min: 0,
|
||||
max: 200,
|
||||
stepBig: 10,
|
||||
stepSmall: 1,
|
||||
unit: 'm',
|
||||
onChanged: changes.add,
|
||||
),
|
||||
const SizedBox(height: 400),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -42));
|
||||
await tester.pump();
|
||||
|
||||
expect(changes, isNotEmpty);
|
||||
expect(changes.last, 80);
|
||||
});
|
||||
|
||||
testWidgets('zeigt den "modified"-Punkt nur, wenn der Wert vom Ausgangswert abweicht',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: (_) {}));
|
||||
var opacity = tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity));
|
||||
expect(opacity.opacity, 0);
|
||||
|
||||
await tester.pumpWidget(wrap(value: 60, initialValue: 50, onChanged: (_) {}));
|
||||
opacity = tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity));
|
||||
expect(opacity.opacity, 1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user