diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 34a9c4bf2..252080994 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -802,17 +802,14 @@ class MapInteractiveViewerState extends State // gestures where the user changes direction during the drag. final flingOffset = _focalStartLocal - _lastFocalLocal; final finalSegment = _prevFocalLocal - _lastFocalLocal; - final finalSegmentDistance = finalSegment.distance; - // Use final segment direction if available, otherwise fall back to overall - // direction for edge cases where the final segment has no movement. - final Offset direction; - if (finalSegmentDistance > 0) { - direction = finalSegment / finalSegmentDistance; - } else { - final flingDistance = flingOffset.distance; - direction = flingOffset / flingDistance; - } + final direction = flingDirection( + finalSegment: finalSegment, + flingOffset: flingOffset, + // `magnitude` is checked to be non-zero above, so this is always a + // finite, non-zero-length direction and is a safe final fallback. + velocityDirection: details.velocity.pixelsPerSecond / magnitude, + ); final distance = (Offset.zero & _camera.nonRotatedSize).shortestSide; _flingAnimation = Tween( @@ -831,6 +828,31 @@ class MapInteractiveViewerState extends State )); } + /// Calculates the direction a fling gesture should continue in. + /// + /// Prefers the direction of the final tracked pointer segment, falling + /// back to the overall drag direction. If both [finalSegment] and + /// [flingOffset] have zero length - which can happen even when the + /// gesture recognizer reports a velocity above the fling threshold, since + /// the velocity is fitted over multiple recent samples and is not + /// necessarily proportional to the last tracked position deltas - falls + /// back to [velocityDirection] to avoid a division by zero (which would + /// otherwise produce a `NaN` direction and corrupt the camera position). + @visibleForTesting + static Offset flingDirection({ + required Offset finalSegment, + required Offset flingOffset, + required Offset velocityDirection, + }) { + final finalSegmentDistance = finalSegment.distance; + if (finalSegmentDistance > 0) return finalSegment / finalSegmentDistance; + + final flingDistance = flingOffset.distance; + if (flingDistance > 0) return flingOffset / flingDistance; + + return velocityDirection; + } + void _handleTap(TapPosition position) { if (_ckrTriggered.value) return; diff --git a/test/gestures/map_interactive_viewer_test.dart b/test/gestures/map_interactive_viewer_test.dart new file mode 100644 index 000000000..b7ee28728 --- /dev/null +++ b/test/gestures/map_interactive_viewer_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_map/src/gestures/map_interactive_viewer.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('MapInteractiveViewerState.flingDirection', () { + test('uses the final segment direction when it has non-zero length', () { + final direction = MapInteractiveViewerState.flingDirection( + finalSegment: const Offset(10, 0), + flingOffset: const Offset(100, 0), + velocityDirection: const Offset(0, 1), + ); + + expect(direction, const Offset(1, 0)); + }); + + test( + 'falls back to the overall drag direction when the final segment has ' + 'zero length', + () { + final direction = MapInteractiveViewerState.flingDirection( + finalSegment: Offset.zero, + flingOffset: const Offset(0, -50), + velocityDirection: const Offset(1, 0), + ); + + expect(direction, const Offset(0, -1)); + }, + ); + + test( + 'falls back to the velocity direction instead of dividing by zero ' + 'when both the final segment and the overall drag offset have zero ' + 'length (regression test: this previously produced a NaN direction, ' + 'which corrupted the camera position - ' + 'https://github.com/fleaflet/flutter_map/issues/2199)', + () { + final direction = MapInteractiveViewerState.flingDirection( + finalSegment: Offset.zero, + flingOffset: Offset.zero, + velocityDirection: const Offset(0.6, 0.8), + ); + + expect(direction, const Offset(0.6, 0.8)); + expect(direction.dx.isFinite, isTrue); + expect(direction.dy.isFinite, isTrue); + }, + ); + }); +}