Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix interference in fling-scrolling from cross-axis motion #122338

Merged
merged 2 commits into from
Mar 17, 2023
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
8 changes: 6 additions & 2 deletions packages/flutter/lib/src/gestures/drag_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,18 @@ typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
class DragEndDetails {
/// Creates details for a [GestureDragEndCallback].
///
/// If [primaryVelocity] is non-null, its value must match one of the
/// coordinates of `velocity.pixelsPerSecond` and the other coordinate
/// must be zero.
///
/// The [velocity] argument must not be null.
DragEndDetails({
this.velocity = Velocity.zero,
this.primaryVelocity,
}) : assert(
primaryVelocity == null
|| primaryVelocity == velocity.pixelsPerSecond.dx
|| primaryVelocity == velocity.pixelsPerSecond.dy,
|| (primaryVelocity == velocity.pixelsPerSecond.dx && velocity.pixelsPerSecond.dy == 0)
|| (primaryVelocity == velocity.pixelsPerSecond.dy && velocity.pixelsPerSecond.dx == 0),
);

/// The velocity the pointer was moving when it stopped contacting the screen.
Expand Down
93 changes: 70 additions & 23 deletions packages/flutter/lib/src/gestures/monodrag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// The distance traveled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [DragUpdateDetails] object.
///
/// If this gesture recognizer recognizes movement on a single axis (a
/// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
/// then `details` will reflect movement only on that axis and its
/// [DragUpdateDetails.primaryDelta] will be non-null.
/// If this gesture recognizer recognizes movement in all directions
/// (a [PanGestureRecognizer]), then `details` will reflect movement on
/// both axes and its [DragUpdateDetails.primaryDelta] will be null.
///
/// See also:
///
/// * [allowedButtonsFilter], which decides which button will be allowed.
Expand All @@ -162,6 +170,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// The velocity is provided in the callback's `details` argument, which is a
/// [DragEndDetails] object.
///
/// If this gesture recognizer recognizes movement on a single axis (a
/// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
/// then `details` will reflect movement only on that axis and its
/// [DragEndDetails.primaryVelocity] will be non-null.
/// If this gesture recognizer recognizes movement in all directions
/// (a [PanGestureRecognizer]), then `details` will reflect movement on
/// both axes and its [DragEndDetails.primaryVelocity] will be null.
///
/// See also:
///
/// * [allowedButtonsFilter], which decides which button will be allowed.
Expand Down Expand Up @@ -258,6 +274,13 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// inertia, for example.
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind);

/// Determines if a gesture is a fling or not, and if so its effective velocity.
///
/// A fling calls its gesture end callback with a velocity, allowing the
/// provider of the callback to respond by carrying the gesture forward with
/// inertia, for example.
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind);

Offset _getDeltaForDetails(Offset delta);
double? _getPrimaryValueFromOffset(Offset value);
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);
Expand Down Expand Up @@ -504,33 +527,21 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}

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

final DragEndDetails details;
DragEndDetails? details;
final String Function() debugReport;

final VelocityEstimate? estimate = tracker.getVelocityEstimate();
if (estimate != null && isFlingGesture(estimate, tracker.kind)) {
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
details = DragEndDetails(
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
);
debugReport = () {
return '$estimate; fling at $velocity.';
};
if (estimate == null) {
debugReport = () => 'Could not estimate velocity.';
} else {
details = DragEndDetails(
primaryVelocity: 0.0,
);
debugReport = () {
if (estimate == null) {
return 'Could not estimate velocity.';
}
return '$estimate; judged to not be a fling.';
};
details = _considerFling(estimate, tracker.kind);
debugReport = (details != null)
? () => '$estimate; fling at ${details!.velocity}.'
: () => '$estimate; judged to not be a fling.';
}
invokeCallback<void>('onEnd', () => onEnd!(details), debugReport: debugReport);
details ??= DragEndDetails(primaryVelocity: 0.0);

invokeCallback<void>('onEnd', () => onEnd!(details!), debugReport: debugReport);
}

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

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
final double dy = clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity);
return DragEndDetails(
velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
primaryVelocity: dy,
);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
Expand Down Expand Up @@ -620,6 +644,19 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
}

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
final double dx = clampDouble(estimate.pixelsPerSecond.dx, -maxVelocity, maxVelocity);
return DragEndDetails(
velocity: Velocity(pixelsPerSecond: Offset(dx, 0)),
primaryVelocity: dx,
);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
Expand Down Expand Up @@ -660,6 +697,16 @@ class PanGestureRecognizer extends DragGestureRecognizer {
&& estimate.offset.distanceSquared > minDistance * minDistance;
}

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
return DragEndDetails(velocity: velocity);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
Expand Down
80 changes: 80 additions & 0 deletions packages/flutter/test/gestures/drag_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,86 @@ void main() {
expect(primaryVelocity, velocity.pixelsPerSecond.dx);
});

/// Drag the pointer at the given velocity, and return the details
/// the recognizer passes to onEnd.
///
/// This method will mutate `recognizer.onEnd`.
DragEndDetails performDragToEnd(GestureTester tester, DragGestureRecognizer recognizer, Offset pointerVelocity) {
late DragEndDetails actual;
recognizer.onEnd = (DragEndDetails details) {
actual = details;
};
final TestPointer pointer = TestPointer();
final PointerDownEvent down = pointer.down(Offset.zero);
recognizer.addPointer(down);
tester.closeArena(pointer.pointer);
tester.route(down);
tester.route(pointer.move(pointerVelocity * 0.025, timeStamp: const Duration(milliseconds: 25)));
tester.route(pointer.move(pointerVelocity * 0.050, timeStamp: const Duration(milliseconds: 50)));
tester.route(pointer.up(timeStamp: const Duration(milliseconds: 50)));
return actual;
}

testGesture('Clamp max pan velocity in 2D, isotropically', (GestureTester tester) {
final PanGestureRecognizer recognizer = PanGestureRecognizer();
addTearDown(recognizer.dispose);

void checkDrag(Offset pointerVelocity, Offset expectedVelocity) {
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
expect(actual.velocity.pixelsPerSecond, offsetMoreOrLessEquals(expectedVelocity, epsilon: 0.1));
expect(actual.primaryVelocity, isNull);
}

checkDrag(const Offset( 400.0, 400.0), const Offset( 400.0, 400.0));
checkDrag(const Offset( 2000.0, -2000.0), const Offset( 2000.0, -2000.0));
checkDrag(const Offset(-8000.0, -8000.0), const Offset(-5656.9, -5656.9));
checkDrag(const Offset(-8000.0, 6000.0), const Offset(-6400.0, 4800.0));
checkDrag(const Offset(-9000.0, 0.0), const Offset(-8000.0, 0.0));
checkDrag(const Offset(-9000.0, -1000.0), const Offset(-7951.1, - 883.5));
checkDrag(const Offset(-1000.0, 9000.0), const Offset(- 883.5, 7951.1));
checkDrag(const Offset( 0.0, 9000.0), const Offset( 0.0, 8000.0));
});

testGesture('Clamp max vertical-drag velocity vertically', (GestureTester tester) {
final VerticalDragGestureRecognizer recognizer = VerticalDragGestureRecognizer();
addTearDown(recognizer.dispose);

void checkDrag(Offset pointerVelocity, double expectedVelocity) {
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
expect(actual.primaryVelocity, moreOrLessEquals(expectedVelocity, epsilon: 0.1));
expect(actual.velocity.pixelsPerSecond.dx, 0.0);
expect(actual.velocity.pixelsPerSecond.dy, actual.primaryVelocity);
}

checkDrag(const Offset( 500.0, 400.0), 400.0);
checkDrag(const Offset( 3000.0, -2000.0), -2000.0);
checkDrag(const Offset(-9000.0, -9000.0), -8000.0);
checkDrag(const Offset(-9000.0, 0.0), 0.0);
checkDrag(const Offset(-9000.0, 1000.0), 1000.0);
checkDrag(const Offset(-1000.0, -9000.0), -8000.0);
checkDrag(const Offset( 0.0, -9000.0), -8000.0);
});

testGesture('Clamp max horizontal-drag velocity horizontally', (GestureTester tester) {
final HorizontalDragGestureRecognizer recognizer = HorizontalDragGestureRecognizer();
addTearDown(recognizer.dispose);

void checkDrag(Offset pointerVelocity, double expectedVelocity) {
final DragEndDetails actual = performDragToEnd(tester, recognizer, pointerVelocity);
expect(actual.primaryVelocity, moreOrLessEquals(expectedVelocity, epsilon: 0.1));
expect(actual.velocity.pixelsPerSecond.dx, actual.primaryVelocity);
expect(actual.velocity.pixelsPerSecond.dy, 0.0);
}

checkDrag(const Offset( 500.0, 400.0), 500.0);
checkDrag(const Offset( 3000.0, -2000.0), 3000.0);
checkDrag(const Offset(-9000.0, -9000.0), -8000.0);
checkDrag(const Offset(-9000.0, 0.0), -8000.0);
checkDrag(const Offset(-9000.0, 1000.0), -8000.0);
checkDrag(const Offset(-1000.0, -9000.0), -1000.0);
checkDrag(const Offset( 0.0, -9000.0), 0.0);
});

testGesture('Synthesized pointer events are ignored for velocity tracking', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Expand Down