Skip to content
Open
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
31 changes: 28 additions & 3 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
bool _dragMode = false;
int _gestureWinner = MultiFingerGesture.none;
int _pointerCounter = 0;
PointerDeviceKind? _lastPointerKind;
bool _isListeningForInterruptions = false;

var _rotationStarted = false;
Expand Down Expand Up @@ -372,6 +373,7 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

void _onPointerDown(PointerDownEvent event) {
++_pointerCounter;
_lastPointerKind = event.kind;

if (_ckrTriggered.value) {
_ckrInitialDegrees = _camera.rotation;
Expand Down Expand Up @@ -808,7 +810,16 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
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,
// Negated so that it shares the convention of the tracked offsets,
// which run opposite to the pointer motion.
velocityDirection: -details.velocity.pixelsPerSecond / magnitude,
// On touch devices the pointer cannot leave the window mid-drag (the
// case the tracked offsets handle better than the velocity, see
// #2158), and the velocity - fitted over multiple recent samples - is
// far more stable than the final tracked segment: deriving the
// direction from the single last, noisy pointer segment sends fast
// swipes off-axis, so repeated flicks visibly zigzag.
preferVelocityDirection: _lastPointerKind == PointerDeviceKind.touch,
);
final distance = (Offset.zero & _camera.nonRotatedSize).shortestSide;

Expand All @@ -830,8 +841,19 @@ 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
/// All three candidate vectors must share the same convention: the
/// direction the tracked pointer offsets travel in, i.e. opposite to the
/// pointer motion.
///
/// With [preferVelocityDirection] the (already normalized)
/// [velocityDirection] is used directly: the velocity is fitted over
/// multiple recent samples and is therefore much more stable than the
/// final tracked segment. This is the right choice for touch input, where
/// the pointer cannot leave the window mid-drag.
///
/// Otherwise prefers the direction of the final tracked pointer segment,
/// falling back to the overall drag direction - which handles the pointer
/// leaving the window on web/desktop (#2158). 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
Expand All @@ -843,7 +865,10 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
required Offset finalSegment,
required Offset flingOffset,
required Offset velocityDirection,
required bool preferVelocityDirection,
}) {
if (preferVelocityDirection) return velocityDirection;

final finalSegmentDistance = finalSegment.distance;
if (finalSegmentDistance > 0) return finalSegment / finalSegmentDistance;

Expand Down
21 changes: 21 additions & 0 deletions test/gestures/map_interactive_viewer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,30 @@ import 'package:flutter_test/flutter_test.dart';

void main() {
group('MapInteractiveViewerState.flingDirection', () {
test(
'uses the velocity direction when preferred, even when the final '
'segment has non-zero length (touch input: the velocity is fitted '
'over multiple recent samples, while the single last pointer segment '
'is noisy on fast swipes and sends repeated flicks zigzagging '
'off-axis)',
() {
final direction = MapInteractiveViewerState.flingDirection(
finalSegment: const Offset(10, 2),
flingOffset: const Offset(100, 0),
velocityDirection: const Offset(0, 1),
preferVelocityDirection: true,
);

expect(direction, const Offset(0, 1));
},
);

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),
preferVelocityDirection: false,
);

expect(direction, const Offset(1, 0));
Expand All @@ -21,6 +40,7 @@ void main() {
finalSegment: Offset.zero,
flingOffset: const Offset(0, -50),
velocityDirection: const Offset(1, 0),
preferVelocityDirection: false,
);

expect(direction, const Offset(0, -1));
Expand All @@ -38,6 +58,7 @@ void main() {
finalSegment: Offset.zero,
flingOffset: Offset.zero,
velocityDirection: const Offset(0.6, 0.8),
preferVelocityDirection: false,
);

expect(direction, const Offset(0.6, 0.8));
Expand Down