Skip to content

Commit 1ac7333

Browse files
authored
Reland VelocityTracker update (#132291) (#137381)
This updates the implementation to use the stopwatch from the Clock object and piping it through to the TestWidgetsFlutterBinding so it will be kept in sync with FakeAsync. Relands #132291 Fixes flutter/flutter#97761 The change was reverted due to flakiness it introduced in tests that use fling gestures. * flutter/flutter#135728
1 parent 7f3f52f commit 1ac7333

File tree

7 files changed

+140
-43
lines changed

7 files changed

+140
-43
lines changed

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
366366

367367
if (resamplingEnabled) {
368368
_resampler.addOrDispatch(event);
369-
_resampler.sample(samplingOffset, _samplingClock);
369+
_resampler.sample(samplingOffset, samplingClock);
370370
return;
371371
}
372372

@@ -505,24 +505,29 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
505505
_hitTests.clear();
506506
}
507507

508-
/// Overrides the sampling clock for debugging and testing.
509-
///
510-
/// This value is ignored in non-debug builds.
511-
@protected
512-
SamplingClock? get debugSamplingClock => null;
513-
514508
void _handleSampleTimeChanged() {
515509
if (!locked) {
516510
if (resamplingEnabled) {
517-
_resampler.sample(samplingOffset, _samplingClock);
511+
_resampler.sample(samplingOffset, samplingClock);
518512
}
519513
else {
520514
_resampler.stop();
521515
}
522516
}
523517
}
524518

525-
SamplingClock get _samplingClock {
519+
/// Overrides the [samplingClock] for debugging and testing.
520+
///
521+
/// This value is ignored in non-debug builds.
522+
@protected
523+
SamplingClock? get debugSamplingClock => null;
524+
525+
/// Provides access to the current [DateTime] and `StopWatch` objects for
526+
/// sampling.
527+
///
528+
/// Overridden by [debugSamplingClock] for debug builds and testing. Using
529+
/// this object under test will maintain synchronization with [FakeAsync].
530+
SamplingClock get samplingClock {
526531
SamplingClock value = SamplingClock();
527532
assert(() {
528533
final SamplingClock? debugValue = debugSamplingClock;

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import 'package:flutter/foundation.dart';
77

8+
import 'binding.dart';
89
import 'events.dart';
910
import 'lsq_solver.dart';
1011

@@ -149,12 +150,21 @@ class VelocityTracker {
149150
/// The kind of pointer this tracker is for.
150151
final PointerDeviceKind kind;
151152

153+
// Time difference since the last sample was added
154+
Stopwatch get _sinceLastSample {
155+
_stopwatch ??= GestureBinding.instance.samplingClock.stopwatch();
156+
return _stopwatch!;
157+
}
158+
Stopwatch? _stopwatch;
159+
152160
// Circular buffer; current sample at _index.
153161
final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null);
154162
int _index = 0;
155163

156164
/// Adds a position as the given time to the tracker.
157165
void addPosition(Duration time, Offset position) {
166+
_sinceLastSample.start();
167+
_sinceLastSample.reset();
158168
_index += 1;
159169
if (_index == _historySize) {
160170
_index = 0;
@@ -169,6 +179,16 @@ class VelocityTracker {
169179
///
170180
/// Returns null if there is no data on which to base an estimate.
171181
VelocityEstimate? getVelocityEstimate() {
182+
// Has user recently moved since last sample?
183+
if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
184+
return const VelocityEstimate(
185+
pixelsPerSecond: Offset.zero,
186+
confidence: 1.0,
187+
duration: Duration.zero,
188+
offset: Offset.zero,
189+
);
190+
}
191+
172192
final List<double> x = <double>[];
173193
final List<double> y = <double>[];
174194
final List<double> w = <double>[];
@@ -195,7 +215,7 @@ class VelocityTracker {
195215
final double age = (newestSample.time - sample.time).inMicroseconds.toDouble() / 1000;
196216
final double delta = (sample.time - previousSample.time).inMicroseconds.abs().toDouble() / 1000;
197217
previousSample = sample;
198-
if (age > _horizonMilliseconds || delta > _assumePointerMoveStoppedMilliseconds) {
218+
if (age > _horizonMilliseconds || delta > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
199219
break;
200220
}
201221

@@ -288,6 +308,8 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
288308

289309
@override
290310
void addPosition(Duration time, Offset position) {
311+
_sinceLastSample.start();
312+
_sinceLastSample.reset();
291313
assert(() {
292314
final _PointAtTime? previousPoint = _touchSamples[_index];
293315
if (previousPoint == null || previousPoint.time <= time) {
@@ -326,6 +348,16 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
326348

327349
@override
328350
VelocityEstimate getVelocityEstimate() {
351+
// Has user recently moved since last sample?
352+
if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
353+
return const VelocityEstimate(
354+
pixelsPerSecond: Offset.zero,
355+
confidence: 1.0,
356+
duration: Duration.zero,
357+
offset: Offset.zero,
358+
);
359+
}
360+
329361
// The velocity estimated using this expression is an approximation of the
330362
// scroll velocity of an iOS scroll view at the moment the user touch was
331363
// released, not the final velocity of the iOS pan gesture recognizer
@@ -387,6 +419,16 @@ class MacOSScrollViewFlingVelocityTracker extends IOSScrollViewFlingVelocityTrac
387419

388420
@override
389421
VelocityEstimate getVelocityEstimate() {
422+
// Has user recently moved since last sample?
423+
if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
424+
return const VelocityEstimate(
425+
pixelsPerSecond: Offset.zero,
426+
confidence: 1.0,
427+
duration: Duration.zero,
428+
offset: Offset.zero,
429+
);
430+
}
431+
390432
// The velocity estimated using this expression is an approximation of the
391433
// scroll velocity of a macOS scroll view at the moment the user touch was
392434
// released.

packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,18 @@
44

55
import 'dart:ui' as ui;
66

7-
import 'package:clock/clock.dart';
87
import 'package:flutter/gestures.dart';
98
import 'package:flutter/scheduler.dart';
109
import 'package:flutter/widgets.dart';
1110
import 'package:flutter_test/flutter_test.dart';
1211
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
1312

14-
class TestResampleEventFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
15-
@override
16-
SamplingClock? get debugSamplingClock => TestSamplingClock(this.clock);
17-
}
18-
19-
class TestSamplingClock implements SamplingClock {
20-
TestSamplingClock(this._clock);
21-
22-
@override
23-
DateTime now() => _clock.now();
24-
25-
@override
26-
Stopwatch stopwatch() => _clock.stopwatch();
27-
28-
final Clock _clock;
29-
}
3013

3114
void main() {
32-
final TestWidgetsFlutterBinding binding = TestResampleEventFlutterBinding();
3315
testWidgetsWithLeakTracking('PointerEvent resampling on a widget', (WidgetTester tester) async {
34-
assert(WidgetsBinding.instance == binding);
35-
Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch);
16+
Duration currentTestFrameTime() => Duration(
17+
milliseconds: TestWidgetsFlutterBinding.instance.clock.now().millisecondsSinceEpoch,
18+
);
3619
void requestFrame() => SchedulerBinding.instance.scheduleFrameCallback((_) {});
3720
final Duration epoch = currentTestFrameTime();
3821
final ui.PointerDataPacket packet = ui.PointerDataPacket(

packages/flutter/test/gestures/gesture_tester.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'package:fake_async/fake_async.dart';
66
import 'package:flutter/gestures.dart';
7-
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
88
import 'package:meta/meta.dart';
99

1010
class GestureTester {
@@ -26,7 +26,7 @@ typedef GestureTest = void Function(GestureTester tester);
2626

2727
@isTest
2828
void testGesture(String description, GestureTest callback) {
29-
test(description, () {
29+
testWidgetsWithLeakTracking(description, (_) async {
3030
FakeAsync().run((FakeAsync async) {
3131
callback(GestureTester._(async));
3232
});

packages/flutter/test/gestures/velocity_tracker_test.dart

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:flutter/gestures.dart';
66
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
78
import 'velocity_tracker_data.dart';
89

910
bool _withinTolerance(double actual, double expected) {
@@ -34,7 +35,7 @@ void main() {
3435
Offset(-71.51939428321249, 3716.7385187526947),
3536
];
3637

37-
test('Velocity tracker gives expected results', () {
38+
testWidgetsWithLeakTracking('Velocity tracker gives expected results', (WidgetTester tester) async {
3839
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
3940
int i = 0;
4041
for (final PointerEvent event in velocityEventData) {
@@ -48,7 +49,7 @@ void main() {
4849
}
4950
});
5051

51-
test('Velocity control test', () {
52+
testWidgetsWithLeakTracking('Velocity control test', (WidgetTester tester) async {
5253
const Velocity velocity1 = Velocity(pixelsPerSecond: Offset(7.0, 0.0));
5354
const Velocity velocity2 = Velocity(pixelsPerSecond: Offset(12.0, 0.0));
5455
expect(velocity1, equals(const Velocity(pixelsPerSecond: Offset(7.0, 0.0))));
@@ -60,7 +61,7 @@ void main() {
6061
expect(velocity1, hasOneLineDescription);
6162
});
6263

63-
test('Interrupted velocity estimation', () {
64+
testWidgetsWithLeakTracking('Interrupted velocity estimation', (WidgetTester tester) async {
6465
// Regression test for https://github.com/flutter/flutter/pull/7510
6566
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
6667
for (final PointerEvent event in interruptedVelocityEventData) {
@@ -73,12 +74,12 @@ void main() {
7374
}
7475
});
7576

76-
test('No data velocity estimation', () {
77+
testWidgetsWithLeakTracking('No data velocity estimation', (WidgetTester tester) async {
7778
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
7879
expect(tracker.getVelocity(), Velocity.zero);
7980
});
8081

81-
test('FreeScrollStartVelocityTracker.getVelocity throws when no points', () {
82+
testWidgetsWithLeakTracking('FreeScrollStartVelocityTracker.getVelocity throws when no points', (WidgetTester tester) async {
8283
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
8384
AssertionError? exception;
8485
try {
@@ -90,7 +91,7 @@ void main() {
9091
expect(exception?.toString(), contains('at least 1 point'));
9192
});
9293

93-
test('FreeScrollStartVelocityTracker.getVelocity throws when the new point precedes the previous point', () {
94+
testWidgetsWithLeakTracking('FreeScrollStartVelocityTracker.getVelocity throws when the new point precedes the previous point', (WidgetTester tester) async {
9495
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
9596
AssertionError? exception;
9697

@@ -105,7 +106,7 @@ void main() {
105106
expect(exception?.toString(), contains('has a smaller timestamp'));
106107
});
107108

108-
test('Estimate does not throw when there are more than 1 point', () {
109+
testWidgetsWithLeakTracking('Estimate does not throw when there are more than 1 point', (WidgetTester tester) async {
109110
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
110111
Offset position = Offset.zero;
111112
Duration time = Duration.zero;
@@ -127,7 +128,7 @@ void main() {
127128
}
128129
});
129130

130-
test('Makes consistent velocity estimates with consistent velocity', () {
131+
testWidgetsWithLeakTracking('Makes consistent velocity estimates with consistent velocity', (WidgetTester tester) async {
131132
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
132133
Offset position = Offset.zero;
133134
Duration time = Duration.zero;
@@ -144,4 +145,55 @@ void main() {
144145
}
145146
}
146147
});
148+
149+
testWidgetsWithLeakTracking('Assume zero velocity when there are no recent samples - base VelocityTracker', (WidgetTester tester) async {
150+
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
151+
Offset position = Offset.zero;
152+
Duration time = Duration.zero;
153+
const Offset positionDelta = Offset(0, -1);
154+
const Duration durationDelta = Duration(seconds: 1);
155+
156+
for (int i = 0; i < 10; i+=1) {
157+
position += positionDelta;
158+
time += durationDelta;
159+
tracker.addPosition(time, position);
160+
}
161+
await tester.pumpAndSettle();
162+
163+
expect(tracker.getVelocity().pixelsPerSecond, Offset.zero);
164+
});
165+
166+
testWidgetsWithLeakTracking('Assume zero velocity when there are no recent samples - IOS', (WidgetTester tester) async {
167+
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
168+
Offset position = Offset.zero;
169+
Duration time = Duration.zero;
170+
const Offset positionDelta = Offset(0, -1);
171+
const Duration durationDelta = Duration(seconds: 1);
172+
173+
for (int i = 0; i < 10; i+=1) {
174+
position += positionDelta;
175+
time += durationDelta;
176+
tracker.addPosition(time, position);
177+
}
178+
await tester.pumpAndSettle();
179+
180+
expect(tracker.getVelocity().pixelsPerSecond, Offset.zero);
181+
});
182+
183+
testWidgetsWithLeakTracking('Assume zero velocity when there are no recent samples - MacOS', (WidgetTester tester) async {
184+
final MacOSScrollViewFlingVelocityTracker tracker = MacOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
185+
Offset position = Offset.zero;
186+
Duration time = Duration.zero;
187+
const Offset positionDelta = Offset(0, -1);
188+
const Duration durationDelta = Duration(seconds: 1);
189+
190+
for (int i = 0; i < 10; i+=1) {
191+
position += positionDelta;
192+
time += durationDelta;
193+
tracker.addPosition(time, position);
194+
}
195+
await tester.pumpAndSettle();
196+
197+
expect(tracker.getVelocity().pixelsPerSecond, Offset.zero);
198+
});
147199
}

packages/flutter/test/rendering/editable_gesture_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import 'package:flutter/rendering.dart';
88
import 'package:flutter_test/flutter_test.dart';
99

1010
void main() {
11-
setUp(() => _GestureBindingSpy());
12-
13-
test('attach and detach correctly handle gesture', () {
11+
final TestWidgetsFlutterBinding binding = _GestureBindingSpy();
12+
testWidgets('attach and detach correctly handle gesture', (_) async {
13+
assert(WidgetsBinding.instance == binding);
1414
final TextSelectionDelegate delegate = FakeEditableTextState();
1515
final RenderEditable editable = RenderEditable(
1616
backgroundCursorColor: Colors.grey,

packages/flutter_test/lib/src/binding.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
425425
/// actual current wall-clock time.
426426
Clock get clock;
427427

428+
@override
429+
SamplingClock? get debugSamplingClock => _TestSamplingClock(clock);
430+
428431
/// Triggers a frame sequence (build/layout/paint/etc),
429432
/// then flushes microtasks.
430433
///
@@ -2149,6 +2152,18 @@ class TestViewConfiguration extends ViewConfiguration {
21492152
String toString() => 'TestViewConfiguration';
21502153
}
21512154

2155+
class _TestSamplingClock implements SamplingClock {
2156+
_TestSamplingClock(this._clock);
2157+
2158+
@override
2159+
DateTime now() => _clock.now();
2160+
2161+
@override
2162+
Stopwatch stopwatch() => _clock.stopwatch();
2163+
2164+
final Clock _clock;
2165+
}
2166+
21522167
const int _kPointerDecay = -2;
21532168

21542169
class _LiveTestPointerRecord {

0 commit comments

Comments
 (0)