Skip to content

Commit 328f088

Browse files
authored
Fix interference in fling-scrolling from cross-axis motion (flutter#122338)
Fix interference in fling-scrolling from cross-axis motion
1 parent a88eb17 commit 328f088

File tree

3 files changed

+156
-25
lines changed

3 files changed

+156
-25
lines changed

packages/flutter/lib/src/gestures/drag_details.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,18 @@ typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
216216
class DragEndDetails {
217217
/// Creates details for a [GestureDragEndCallback].
218218
///
219+
/// If [primaryVelocity] is non-null, its value must match one of the
220+
/// coordinates of `velocity.pixelsPerSecond` and the other coordinate
221+
/// must be zero.
222+
///
219223
/// The [velocity] argument must not be null.
220224
DragEndDetails({
221225
this.velocity = Velocity.zero,
222226
this.primaryVelocity,
223227
}) : assert(
224228
primaryVelocity == null
225-
|| primaryVelocity == velocity.pixelsPerSecond.dx
226-
|| primaryVelocity == velocity.pixelsPerSecond.dy,
229+
|| (primaryVelocity == velocity.pixelsPerSecond.dx && velocity.pixelsPerSecond.dy == 0)
230+
|| (primaryVelocity == velocity.pixelsPerSecond.dy && velocity.pixelsPerSecond.dx == 0),
227231
);
228232

229233
/// The velocity the pointer was moving when it stopped contacting the screen.

packages/flutter/lib/src/gestures/monodrag.dart

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
147147
/// The distance traveled by the pointer since the last update is provided in
148148
/// the callback's `details` argument, which is a [DragUpdateDetails] object.
149149
///
150+
/// If this gesture recognizer recognizes movement on a single axis (a
151+
/// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
152+
/// then `details` will reflect movement only on that axis and its
153+
/// [DragUpdateDetails.primaryDelta] will be non-null.
154+
/// If this gesture recognizer recognizes movement in all directions
155+
/// (a [PanGestureRecognizer]), then `details` will reflect movement on
156+
/// both axes and its [DragUpdateDetails.primaryDelta] will be null.
157+
///
150158
/// See also:
151159
///
152160
/// * [allowedButtonsFilter], which decides which button will be allowed.
@@ -162,6 +170,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
162170
/// The velocity is provided in the callback's `details` argument, which is a
163171
/// [DragEndDetails] object.
164172
///
173+
/// If this gesture recognizer recognizes movement on a single axis (a
174+
/// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
175+
/// then `details` will reflect movement only on that axis and its
176+
/// [DragEndDetails.primaryVelocity] will be non-null.
177+
/// If this gesture recognizer recognizes movement in all directions
178+
/// (a [PanGestureRecognizer]), then `details` will reflect movement on
179+
/// both axes and its [DragEndDetails.primaryVelocity] will be null.
180+
///
165181
/// See also:
166182
///
167183
/// * [allowedButtonsFilter], which decides which button will be allowed.
@@ -258,6 +274,13 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
258274
/// inertia, for example.
259275
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind);
260276

277+
/// Determines if a gesture is a fling or not, and if so its effective velocity.
278+
///
279+
/// A fling calls its gesture end callback with a velocity, allowing the
280+
/// provider of the callback to respond by carrying the gesture forward with
281+
/// inertia, for example.
282+
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind);
283+
261284
Offset _getDeltaForDetails(Offset delta);
262285
double? _getPrimaryValueFromOffset(Offset value);
263286
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);
@@ -504,33 +527,21 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
504527
}
505528

506529
final VelocityTracker tracker = _velocityTrackers[pointer]!;
530+
final VelocityEstimate? estimate = tracker.getVelocityEstimate();
507531

508-
final DragEndDetails details;
532+
DragEndDetails? details;
509533
final String Function() debugReport;
510-
511-
final VelocityEstimate? estimate = tracker.getVelocityEstimate();
512-
if (estimate != null && isFlingGesture(estimate, tracker.kind)) {
513-
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
514-
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
515-
details = DragEndDetails(
516-
velocity: velocity,
517-
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
518-
);
519-
debugReport = () {
520-
return '$estimate; fling at $velocity.';
521-
};
534+
if (estimate == null) {
535+
debugReport = () => 'Could not estimate velocity.';
522536
} else {
523-
details = DragEndDetails(
524-
primaryVelocity: 0.0,
525-
);
526-
debugReport = () {
527-
if (estimate == null) {
528-
return 'Could not estimate velocity.';
529-
}
530-
return '$estimate; judged to not be a fling.';
531-
};
537+
details = _considerFling(estimate, tracker.kind);
538+
debugReport = (details != null)
539+
? () => '$estimate; fling at ${details!.velocity}.'
540+
: () => '$estimate; judged to not be a fling.';
532541
}
533-
invokeCallback<void>('onEnd', () => onEnd!(details), debugReport: debugReport);
542+
details ??= DragEndDetails(primaryVelocity: 0.0);
543+
544+
invokeCallback<void>('onEnd', () => onEnd!(details!), debugReport: debugReport);
534545
}
535546

536547
void _checkCancel() {
@@ -578,6 +589,19 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
578589
return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
579590
}
580591

592+
@override
593+
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
594+
if (!isFlingGesture(estimate, kind)) {
595+
return null;
596+
}
597+
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
598+
final double dy = clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity);
599+
return DragEndDetails(
600+
velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
601+
primaryVelocity: dy,
602+
);
603+
}
604+
581605
@override
582606
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
583607
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
@@ -620,6 +644,19 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
620644
return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
621645
}
622646

647+
@override
648+
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
649+
if (!isFlingGesture(estimate, kind)) {
650+
return null;
651+
}
652+
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
653+
final double dx = clampDouble(estimate.pixelsPerSecond.dx, -maxVelocity, maxVelocity);
654+
return DragEndDetails(
655+
velocity: Velocity(pixelsPerSecond: Offset(dx, 0)),
656+
primaryVelocity: dx,
657+
);
658+
}
659+
623660
@override
624661
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
625662
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
@@ -660,6 +697,16 @@ class PanGestureRecognizer extends DragGestureRecognizer {
660697
&& estimate.offset.distanceSquared > minDistance * minDistance;
661698
}
662699

700+
@override
701+
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
702+
if (!isFlingGesture(estimate, kind)) {
703+
return null;
704+
}
705+
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
706+
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
707+
return DragEndDetails(velocity: velocity);
708+
}
709+
663710
@override
664711
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
665712
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);

packages/flutter/test/gestures/drag_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,86 @@ void main() {
569569
expect(primaryVelocity, velocity.pixelsPerSecond.dx);
570570
});
571571

572+
/// Drag the pointer at the given velocity, and return the details
573+
/// the recognizer passes to onEnd.
574+
///
575+
/// This method will mutate `recognizer.onEnd`.
576+
DragEndDetails performDragToEnd(GestureTester tester, DragGestureRecognizer recognizer, Offset pointerVelocity) {
577+
late DragEndDetails actual;
578+
recognizer.onEnd = (DragEndDetails details) {
579+
actual = details;
580+
};
581+
final TestPointer pointer = TestPointer();
582+
final PointerDownEvent down = pointer.down(Offset.zero);
583+
recognizer.addPointer(down);
584+
tester.closeArena(pointer.pointer);
585+
tester.route(down);
586+
tester.route(pointer.move(pointerVelocity * 0.025, timeStamp: const Duration(milliseconds: 25)));
587+
tester.route(pointer.move(pointerVelocity * 0.050, timeStamp: const Duration(milliseconds: 50)));
588+
tester.route(pointer.up(timeStamp: const Duration(milliseconds: 50)));
589+
return actual;
590+
}
591+
592+
testGesture('Clamp max pan velocity in 2D, isotropically', (GestureTester tester) {
593+
final PanGestureRecognizer recognizer = PanGestureRecognizer();
594+
addTearDown(recognizer.dispose);
595+
596+
void checkDrag(Offset pointerVelocity, Offset expectedVelocity) {
597+
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
598+
expect(actual.velocity.pixelsPerSecond, offsetMoreOrLessEquals(expectedVelocity, epsilon: 0.1));
599+
expect(actual.primaryVelocity, isNull);
600+
}
601+
602+
checkDrag(const Offset( 400.0, 400.0), const Offset( 400.0, 400.0));
603+
checkDrag(const Offset( 2000.0, -2000.0), const Offset( 2000.0, -2000.0));
604+
checkDrag(const Offset(-8000.0, -8000.0), const Offset(-5656.9, -5656.9));
605+
checkDrag(const Offset(-8000.0, 6000.0), const Offset(-6400.0, 4800.0));
606+
checkDrag(const Offset(-9000.0, 0.0), const Offset(-8000.0, 0.0));
607+
checkDrag(const Offset(-9000.0, -1000.0), const Offset(-7951.1, - 883.5));
608+
checkDrag(const Offset(-1000.0, 9000.0), const Offset(- 883.5, 7951.1));
609+
checkDrag(const Offset( 0.0, 9000.0), const Offset( 0.0, 8000.0));
610+
});
611+
612+
testGesture('Clamp max vertical-drag velocity vertically', (GestureTester tester) {
613+
final VerticalDragGestureRecognizer recognizer = VerticalDragGestureRecognizer();
614+
addTearDown(recognizer.dispose);
615+
616+
void checkDrag(Offset pointerVelocity, double expectedVelocity) {
617+
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
618+
expect(actual.primaryVelocity, moreOrLessEquals(expectedVelocity, epsilon: 0.1));
619+
expect(actual.velocity.pixelsPerSecond.dx, 0.0);
620+
expect(actual.velocity.pixelsPerSecond.dy, actual.primaryVelocity);
621+
}
622+
623+
checkDrag(const Offset( 500.0, 400.0), 400.0);
624+
checkDrag(const Offset( 3000.0, -2000.0), -2000.0);
625+
checkDrag(const Offset(-9000.0, -9000.0), -8000.0);
626+
checkDrag(const Offset(-9000.0, 0.0), 0.0);
627+
checkDrag(const Offset(-9000.0, 1000.0), 1000.0);
628+
checkDrag(const Offset(-1000.0, -9000.0), -8000.0);
629+
checkDrag(const Offset( 0.0, -9000.0), -8000.0);
630+
});
631+
632+
testGesture('Clamp max horizontal-drag velocity horizontally', (GestureTester tester) {
633+
final HorizontalDragGestureRecognizer recognizer = HorizontalDragGestureRecognizer();
634+
addTearDown(recognizer.dispose);
635+
636+
void checkDrag(Offset pointerVelocity, double expectedVelocity) {
637+
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
638+
expect(actual.primaryVelocity, moreOrLessEquals(expectedVelocity, epsilon: 0.1));
639+
expect(actual.velocity.pixelsPerSecond.dx, actual.primaryVelocity);
640+
expect(actual.velocity.pixelsPerSecond.dy, 0.0);
641+
}
642+
643+
checkDrag(const Offset( 500.0, 400.0), 500.0);
644+
checkDrag(const Offset( 3000.0, -2000.0), 3000.0);
645+
checkDrag(const Offset(-9000.0, -9000.0), -8000.0);
646+
checkDrag(const Offset(-9000.0, 0.0), -8000.0);
647+
checkDrag(const Offset(-9000.0, 1000.0), -8000.0);
648+
checkDrag(const Offset(-1000.0, -9000.0), -1000.0);
649+
checkDrag(const Offset( 0.0, -9000.0), 0.0);
650+
});
651+
572652
testGesture('Synthesized pointer events are ignored for velocity tracking', (GestureTester tester) {
573653
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
574654
addTearDown(drag.dispose);

0 commit comments

Comments
 (0)