diff --git a/packages/flutter/lib/src/gestures/velocity_tracker.dart b/packages/flutter/lib/src/gestures/velocity_tracker.dart index ca7aa468a0..f761a1be23 100644 --- a/packages/flutter/lib/src/gestures/velocity_tracker.dart +++ b/packages/flutter/lib/src/gestures/velocity_tracker.dart @@ -149,12 +149,17 @@ class VelocityTracker { /// The kind of pointer this tracker is for. final PointerDeviceKind kind; + // Time difference since the last sample was added + final Stopwatch _sinceLastSample = Stopwatch(); + // Circular buffer; current sample at _index. final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null); int _index = 0; /// Adds a position as the given time to the tracker. void addPosition(Duration time, Offset position) { + _sinceLastSample.start(); + _sinceLastSample.reset(); _index += 1; if (_index == _historySize) { _index = 0; @@ -169,6 +174,16 @@ class VelocityTracker { /// /// Returns null if there is no data on which to base an estimate. VelocityEstimate? getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + final List x = []; final List y = []; final List w = []; @@ -195,7 +210,7 @@ class VelocityTracker { final double age = (newestSample.time - sample.time).inMicroseconds.toDouble() / 1000; final double delta = (sample.time - previousSample.time).inMicroseconds.abs().toDouble() / 1000; previousSample = sample; - if (age > _horizonMilliseconds || delta > _assumePointerMoveStoppedMilliseconds) { + if (age > _horizonMilliseconds || delta > VelocityTracker._assumePointerMoveStoppedMilliseconds) { break; } @@ -288,6 +303,8 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker { @override void addPosition(Duration time, Offset position) { + _sinceLastSample.start(); + _sinceLastSample.reset(); assert(() { final _PointAtTime? previousPoint = _touchSamples[_index]; if (previousPoint == null || previousPoint.time <= time) { @@ -326,6 +343,16 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker { @override VelocityEstimate getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + // The velocity estimated using this expression is an approximation of the // scroll velocity of an iOS scroll view at the moment the user touch was // released, not the final velocity of the iOS pan gesture recognizer @@ -387,6 +414,16 @@ class MacOSScrollViewFlingVelocityTracker extends IOSScrollViewFlingVelocityTrac @override VelocityEstimate getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + // The velocity estimated using this expression is an approximation of the // scroll velocity of a macOS scroll view at the moment the user touch was // released. diff --git a/packages/flutter/test/gestures/velocity_tracker_test.dart b/packages/flutter/test/gestures/velocity_tracker_test.dart index 3e6b94534b..61eebc13fe 100644 --- a/packages/flutter/test/gestures/velocity_tracker_test.dart +++ b/packages/flutter/test/gestures/velocity_tracker_test.dart @@ -144,4 +144,22 @@ void main() { } } }); + + test('Assume zero velocity when there are no recent samples', () async { + final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch); + Offset position = Offset.zero; + Duration time = Duration.zero; + const Offset positionDelta = Offset(0, -1); + const Duration durationDelta = Duration(seconds: 1); + + for (int i = 0; i < 10; i+=1) { + position += positionDelta; + time += durationDelta; + tracker.addPosition(time, position); + } + + await Future.delayed(const Duration(milliseconds: 50)); + + expect(tracker.getVelocity().pixelsPerSecond, Offset.zero); + }); }