Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -802,17 +802,14 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
// 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<Offset>(
Expand All @@ -831,6 +828,31 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
));
}

/// 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;

Expand Down
49 changes: 49 additions & 0 deletions test/gestures/map_interactive_viewer_test.dart
Original file line number Diff line number Diff line change
@@ -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);
},
);
});
}
Loading