FB-10: close the ambient-tick-vs-drag race that snapped the map back

A pointer down/up Listener around the FlutterMap now tracks whether a real
gesture is in progress, and didUpdateWidget's follow-recenter callbacks skip
the camera move while one is -- closing the timing window where a once-per-
second ambient GPS tick's postFrameCallback could land after a finger touched
the map but before flutter_map reported hasGesture: true, snapping the camera
back out from under an in-progress pan.
This commit is contained in:
2026-08-25 11:34:05 -05:00
parent 4051416add
commit dde6ec833a
3 changed files with 417 additions and 79 deletions

View File

@@ -520,6 +520,79 @@ void main() {
expect(center.longitude, closeTo(fix1.longitude, 1e-9));
});
testWidgets(
'a real drag beats an ambient tick that lands mid-drag (FB-10)',
(tester) async {
const fix1 = ll.LatLng(51.0, -114.0);
// The ambient tick that lands while the finger is down but before
// flutter_map's own gesture recognizer has reported `hasGesture: true` --
// this is the exact race the ticket describes.
const fix2 = ll.LatLng(51.5, -114.5);
Widget build(ll.LatLng ambient) => MaterialApp(
theme: ripprTheme(),
home: Scaffold(
body: RideMap(
points: const [],
segments: const [],
showEmptyLabel: false,
follow: true,
ambientPosition: ambient,
),
),
);
await tester.pumpWidget(build(fix1));
await tester.pump();
// Put a real pointer down on the map -- this is what a rider's finger
// touching the screen looks like, well before flutter_map decides the
// movement counts as a drag.
final gesture =
await tester.startGesture(tester.getCenter(find.byType(FlutterMap)));
addTearDown(() => gesture.removePointer());
// Simulate a once-per-second ambient GPS tick landing while the pointer
// is already down but before it has moved -- exactly the race window
// the ticket describes: flutter_map has not yet reported `hasGesture:
// true`, so nothing has flipped `_following` off yet.
await tester.pumpWidget(build(fix2));
await tester.pump();
// The critical assertion: with the pointer still down and untouched by
// any real drag, the camera must not have snapped to the ambient tick's
// position. Without the fix, the race lets the scheduled
// `postFrameCallback` win here and the camera jumps to `fix2` before
// the rider's finger has moved at all.
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
var center = map.mapController!.camera.center;
expect(
center.latitude,
isNot(closeTo(fix2.latitude, 1e-6)),
reason: 'the mid-drag ambient tick must not win the race and snap the '
'camera back to the ambient position while the gesture is still '
'down',
);
expect(center.longitude, isNot(closeTo(fix2.longitude, 1e-6)));
// Now the finger actually moves and lifts -- the real drag completes
// normally, and the final position reflects it, not the ambient tick.
await gesture.moveBy(const Offset(-100, -100));
await tester.pump();
await gesture.up();
await tester.pump();
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
center = map.mapController!.camera.center;
expect(
center.latitude,
isNot(closeTo(fix2.latitude, 1e-6)),
reason: 'the completed drag must still reflect the rider\'s own pan, '
'not the ambient position the mid-drag tick tried to recenter to',
);
expect(center.longitude, isNot(closeTo(fix2.longitude, 1e-6)));
});
testWidgets('tapping recenter moves the camera to the latest recorded '
'point when recording', (tester) async {
final points = [for (var i = 0; i < 4; i++) p(1, i)];