Skip to content

Commit 43c9990

Browse files
Piinkscaseycrogers
authored andcommitted
Reland VelocityTracker update (again) (flutter#138843)
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 flutter#137381 which attempted to reland flutter#132291 Fixes flutter#97761 The original change was reverted due to flakiness it introduced in tests that use fling gestures. * flutter#135728 It was reverted again due to a change in the leak tracking tests that broke it.
1 parent 3192365 commit 43c9990

File tree

7 files changed

+141
-43
lines changed

7 files changed

+141
-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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,17 @@
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-
}
30-
3113
void main() {
32-
final TestWidgetsFlutterBinding binding = TestResampleEventFlutterBinding();
3314
testWidgetsWithLeakTracking('PointerEvent resampling on a widget', (WidgetTester tester) async {
34-
assert(WidgetsBinding.instance == binding);
35-
Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch);
15+
Duration currentTestFrameTime() => Duration(
16+
milliseconds: TestWidgetsFlutterBinding.instance.clock.now().millisecondsSinceEpoch,
17+
);
3618
void requestFrame() => SchedulerBinding.instance.scheduleFrameCallback((_) {});
3719
final Duration epoch = currentTestFrameTime();
3820
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: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
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';
8+
79
import 'velocity_tracker_data.dart';
810

911
bool _withinTolerance(double actual, double expected) {
@@ -34,7 +36,7 @@ void main() {
3436
Offset(-71.51939428321249, 3716.7385187526947),
3537
];
3638

37-
test('Velocity tracker gives expected results', () {
39+
testWidgetsWithLeakTracking('Velocity tracker gives expected results', (WidgetTester tester) async {
3840
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
3941
int i = 0;
4042
for (final PointerEvent event in velocityEventData) {
@@ -48,7 +50,7 @@ void main() {
4850
}
4951
});
5052

51-
test('Velocity control test', () {
53+
testWidgetsWithLeakTracking('Velocity control test', (WidgetTester tester) async {
5254
const Velocity velocity1 = Velocity(pixelsPerSecond: Offset(7.0, 0.0));
5355
const Velocity velocity2 = Velocity(pixelsPerSecond: Offset(12.0, 0.0));
5456
expect(velocity1, equals(const Velocity(pixelsPerSecond: Offset(7.0, 0.0))));
@@ -60,7 +62,7 @@ void main() {
6062
expect(velocity1, hasOneLineDescription);
6163
});
6264

63-
test('Interrupted velocity estimation', () {
65+
testWidgetsWithLeakTracking('Interrupted velocity estimation', (WidgetTester tester) async {
6466
// Regression test for https://github.com/flutter/flutter/pull/7510
6567
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
6668
for (final PointerEvent event in interruptedVelocityEventData) {
@@ -73,12 +75,12 @@ void main() {
7375
}
7476
});
7577

76-
test('No data velocity estimation', () {
78+
testWidgetsWithLeakTracking('No data velocity estimation', (WidgetTester tester) async {
7779
final VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
7880
expect(tracker.getVelocity(), Velocity.zero);
7981
});
8082

81-
test('FreeScrollStartVelocityTracker.getVelocity throws when no points', () {
83+
testWidgetsWithLeakTracking('FreeScrollStartVelocityTracker.getVelocity throws when no points', (WidgetTester tester) async {
8284
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
8385
AssertionError? exception;
8486
try {
@@ -90,7 +92,7 @@ void main() {
9092
expect(exception?.toString(), contains('at least 1 point'));
9193
});
9294

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

@@ -105,7 +107,7 @@ void main() {
105107
expect(exception?.toString(), contains('has a smaller timestamp'));
106108
});
107109

108-
test('Estimate does not throw when there are more than 1 point', () {
110+
testWidgetsWithLeakTracking('Estimate does not throw when there are more than 1 point', (WidgetTester tester) async {
109111
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
110112
Offset position = Offset.zero;
111113
Duration time = Duration.zero;
@@ -127,7 +129,7 @@ void main() {
127129
}
128130
});
129131

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

packages/flutter/test/rendering/editable_gesture_test.dart

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

1010
void main() {
11-
setUp(() => _GestureBindingSpy());
11+
final TestWidgetsFlutterBinding binding = _GestureBindingSpy();
1212

13-
test('attach and detach correctly handle gesture', () {
13+
testWidgets('attach and detach correctly handle gesture', (_) async {
14+
assert(WidgetsBinding.instance == binding);
1415
final TextSelectionDelegate delegate = FakeEditableTextState();
1516
final RenderEditable editable = RenderEditable(
1617
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)