Auto-scroll waypoint list to active row; fix speed chip step size

Wegpunktliste: der aktive Wegpunkt (per Halo-Menue "Edit" ausgewaehlt)
war bei laengeren Missionen ausserhalb des sichtbaren Bereichs, da die
Liste beim Oeffnen immer oben startete. WaypointListPanel bekommt jetzt
einen ScrollController mit fester Zeilenhoehe (itemExtent, damit die
Zielposition ohne Layout-Messung berechnet werden kann) und scrollt per
post-frame-Callback zur aktiven Zeile - sowohl beim ersten Oeffnen als
auch beim Zurueckwechseln vom Altitude-/Speed-Tab auf List.

WaypointChip (der Swipe-Wertechip in den Listenzeilen) hatte eine fest
einprogrammierte Schrittgroesse von 10 fuer alle Felder (Alt/Speed/
Catch) - fuer Speed bei einer Spanne von nur 13-25 m/s viel zu grob,
ein einzelner Drag-Schritt sprang schon fast durch den gesamten
gueltigen Bereich. Schrittgroesse ist jetzt ein expliziter Parameter;
Speed nutzt DefaultDroneProfile.speedStep (1), Altitude weiterhin
DefaultDroneProfile.altStep (10), Catch-Radius weiterhin 10 (keine
Beschwerde hierzu, Wert unveraendert). Der Alt-/Speed-Drehrad und die
Vollbild-Charts nutzten diese Profile bereits korrekt - der Row-Chip
war die einzige Stelle mit hartcodiertem Wert.

Verifiziert: flutter analyze (0 issues), flutter test (24/24, zwei neue
Tests fuer Auto-Scroll und Speed-Schrittgroesse), manuell auf
Pixel_10a-Emulator mit 7 Wegpunkten - Liste oeffnet direkt bei
Wegpunkt 7, Speed-Chip-Drag aendert 15 m/s in kleinen Schritten (18)
statt in Zehnerspruengen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Constantin Leue
2026-07-28 10:33:25 +02:00
co-authored by Claude Sonnet 5
parent 13da935f99
commit 7a26002a63
3 changed files with 123 additions and 4 deletions
+4 -3
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
/// Swipe-adjustierbarer Wertechip in der Wegpunktliste (HTML-Demonstrator:
/// .value-chip / chipFieldConfig). Vertikal ziehen aendert den Wert in
/// festen Schritten von 10 pro 14px - nach oben erhoeht.
/// festen Schritten von [step] pro 14px - nach oben erhoeht.
class WaypointChip extends StatefulWidget {
const WaypointChip({
super.key,
@@ -10,6 +10,7 @@ class WaypointChip extends StatefulWidget {
required this.unit,
required this.min,
required this.max,
required this.step,
required this.onChanged,
});
@@ -17,9 +18,9 @@ class WaypointChip extends StatefulWidget {
final String unit;
final double min;
final double max;
final double step;
final ValueChanged<double> onChanged;
static const _stepSize = 10.0;
static const _pxPerStep = 14.0;
@override
@@ -41,7 +42,7 @@ class _WaypointChipState extends State<WaypointChip> {
if (startY == null) return;
final deltaY = startY - details.globalPosition.dy;
final steps = (deltaY / WaypointChip._pxPerStep).round();
var val = _dragStartValue + steps * WaypointChip._stepSize;
var val = _dragStartValue + steps * widget.step;
val = val.clamp(widget.min, widget.max);
if (val != widget.value) {
widget.onChanged(val);
+46 -1
View File
@@ -51,6 +51,7 @@ class WaypointListPanel extends ConsumerStatefulWidget {
class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
int _rowEditIndex = -1;
_PanelTab _tab = _PanelTab.list;
final _listScrollController = ScrollController();
static const _headerStyle = TextStyle(
color: Colors.white60,
@@ -59,6 +60,40 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
letterSpacing: 0.5,
);
// Feste Zeilenhoehe, damit die Scrollposition des aktiven Wegpunkts ohne
// Layout-Messung berechnet werden kann (siehe _scrollToActive).
static const _rowExtent = 60.0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActive());
}
@override
void dispose() {
_listScrollController.dispose();
super.dispose();
}
/// Zentriert die Liste auf den aktuell aktiven Wegpunkt (Doku 3.6: das
/// Panel oeffnet sich beim Editieren eines Wegpunkts ueber das
/// Halo-Menue - ohne Auto-Scroll waere er bei laengeren Missionen nicht
/// sichtbar).
void _scrollToActive() {
if (!mounted || widget.activeIndex < 0) return;
if (!_listScrollController.hasClients) return;
final position = _listScrollController.position;
final target = widget.activeIndex * _rowExtent -
position.viewportDimension / 2 +
_rowExtent / 2;
_listScrollController.animateTo(
target.clamp(0.0, position.maxScrollExtent),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
@override
Widget build(BuildContext context) {
final waypoints = ref.watch(currentMissionProvider);
@@ -140,7 +175,12 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
final active = _tab == tab;
return Expanded(
child: InkWell(
onTap: () => setState(() => _tab = tab),
onTap: () {
setState(() => _tab = tab);
if (tab == _PanelTab.list) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActive());
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
@@ -220,7 +260,9 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
),
)
: ListView.builder(
controller: _listScrollController,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemExtent: _rowExtent,
itemCount: waypoints.length,
itemBuilder: (context, index) =>
_row(waypoints[index], index, routeGeometry),
@@ -273,6 +315,7 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
unit: ' m',
min: DefaultDroneProfile.altMin,
max: DefaultDroneProfile.altMax,
step: DefaultDroneProfile.altStep,
onChanged: (v) => widget.onAltitudeChanged(index, v),
)
: Text('${wp.altitudeM.round()} m',
@@ -285,6 +328,7 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
unit: ' m/s',
min: DefaultDroneProfile.speedMin,
max: DefaultDroneProfile.speedMax,
step: DefaultDroneProfile.speedStep,
onChanged: (v) => widget.onSpeedChanged(index, v),
)
: Text('${wp.speedMs.round()} m/s',
@@ -325,6 +369,7 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
unit: ' m',
min: 5,
max: 500,
step: 10,
onChanged: (v) => widget.onCatchRadiusChanged(index, v),
)
: Text('${wp.catchRadiusM.round()} m',