Unverified Commit 9133deb9 authored by David Reveman's avatar David Reveman Committed by GitHub

Timer based pointer event resampling (#73042)

Timer based pointer event resampling

This provides two improvements to pointer event resampling:

1. PostFrameCallbacks are used instead of scheduling frames. This
avoids unnecessary rendering work when resampling is used.

2. Resampling continues when frames are not produced. I.e. input
events keep being delivered at a fixed frequency even if frame
production is taking a long time.

This fixes #72924
Co-authored-by: 's avatarDavid Reveman <reveman@google.com>
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>
parent a2a77285
...@@ -21,13 +21,24 @@ import 'resampler.dart'; ...@@ -21,13 +21,24 @@ import 'resampler.dart';
typedef _HandleSampleTimeChangedCallback = void Function(); typedef _HandleSampleTimeChangedCallback = void Function();
/// Class that implements clock used for sampling.
class SamplingClock {
/// Returns current time.
DateTime now() => DateTime.now();
/// Returns a new stopwatch that uses the current time as reported by `this`.
Stopwatch stopwatch() => Stopwatch();
}
// Class that handles resampling of touch events for multiple pointer // Class that handles resampling of touch events for multiple pointer
// devices. // devices.
// //
// The `samplingInterval` is used to determine the approximate next
// time for resampling.
// SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine // SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine
// sample time. // sample time.
class _Resampler { class _Resampler {
_Resampler(this._handlePointerEvent, this._handleSampleTimeChanged); _Resampler(this._handlePointerEvent, this._handleSampleTimeChanged, this._samplingInterval);
// Resamplers used to filter incoming pointer events. // Resamplers used to filter incoming pointer events.
final Map<int, PointerEventResampler> _resamplers = <int, PointerEventResampler>{}; final Map<int, PointerEventResampler> _resamplers = <int, PointerEventResampler>{};
...@@ -35,9 +46,12 @@ class _Resampler { ...@@ -35,9 +46,12 @@ class _Resampler {
// Flag to track if a frame callback has been scheduled. // Flag to track if a frame callback has been scheduled.
bool _frameCallbackScheduled = false; bool _frameCallbackScheduled = false;
// Current frame time for resampling. // Last frame time for resampling.
Duration _frameTime = Duration.zero; Duration _frameTime = Duration.zero;
// Time since `_frameTime` was updated.
Stopwatch _frameTimeAge = Stopwatch();
// Last sample time and time stamp of last event. // Last sample time and time stamp of last event.
// //
// Only used for debugPrint of resampling margin. // Only used for debugPrint of resampling margin.
...@@ -50,12 +64,18 @@ class _Resampler { ...@@ -50,12 +64,18 @@ class _Resampler {
// Callback used to handle sample time changes. // Callback used to handle sample time changes.
final _HandleSampleTimeChangedCallback _handleSampleTimeChanged; final _HandleSampleTimeChangedCallback _handleSampleTimeChanged;
// Interval used for sampling.
final Duration _samplingInterval;
// Timer used to schedule resampling.
Timer? _timer;
// Add `event` for resampling or dispatch it directly if // Add `event` for resampling or dispatch it directly if
// not a touch event. // not a touch event.
void addOrDispatch(PointerEvent event) { void addOrDispatch(PointerEvent event) {
final SchedulerBinding? scheduler = SchedulerBinding.instance; final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null); assert(scheduler != null);
// Add touch event to resampler or dispatch pointer event directly. // Add touch event to resampler or dispatch pointer event directly.
if (event.kind == PointerDeviceKind.touch) { if (event.kind == PointerDeviceKind.touch) {
// Save last event time for debugPrint of resampling margin. // Save last event time for debugPrint of resampling margin.
_lastEventTime = event.timeStamp; _lastEventTime = event.timeStamp;
...@@ -72,25 +92,43 @@ class _Resampler { ...@@ -72,25 +92,43 @@ class _Resampler {
// Sample and dispatch events. // Sample and dispatch events.
// //
// `samplingOffset` is relative to the current frame time, which // The `samplingOffset` is relative to the current frame time, which
// can be in the past when we're not actively resampling. // can be in the past when we're not actively resampling.
// `samplingInterval` is used to determine the approximate next // The `samplingClock` is the clock used to determine frame time age.
// time for resampling. void sample(Duration samplingOffset, SamplingClock clock) {
// `currentSystemFrameTimeStamp` is used to determine the current
// frame time.
void sample(Duration samplingOffset, Duration samplingInterval) {
final SchedulerBinding? scheduler = SchedulerBinding.instance; final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null); assert(scheduler != null);
// Initialize `_frameTime` if needed. This will be used for periodic
// sampling when frame callbacks are not received.
if (_frameTime == Duration.zero) {
_frameTime = Duration(milliseconds: clock.now().millisecondsSinceEpoch);
_frameTimeAge = clock.stopwatch()..start();
}
// Schedule periodic resampling if `_timer` is not already active.
if (_timer?.isActive == false) {
_timer = Timer.periodic(_samplingInterval, (_) => _onSampleTimeChanged());
}
// Calculate the effective frame time by taking the number
// of sampling intervals since last time `_frameTime` was
// updated into account. This allows us to advance sample
// time without having to receive frame callbacks.
final int samplingIntervalUs = _samplingInterval.inMicroseconds;
final int elapsedIntervals = _frameTimeAge.elapsedMicroseconds ~/ samplingIntervalUs;
final int elapsedUs = elapsedIntervals * samplingIntervalUs;
final Duration frameTime = _frameTime + Duration(microseconds: elapsedUs);
// Determine sample time by adding the offset to the current // Determine sample time by adding the offset to the current
// frame time. This is expected to be in the past and not // frame time. This is expected to be in the past and not
// result in any dispatched events unless we're actively // result in any dispatched events unless we're actively
// resampling events. // resampling events.
final Duration sampleTime = _frameTime + samplingOffset; final Duration sampleTime = frameTime + samplingOffset;
// Determine next sample time by adding the sampling interval // Determine next sample time by adding the sampling interval
// to the current sample time. // to the current sample time.
final Duration nextSampleTime = sampleTime + samplingInterval; final Duration nextSampleTime = sampleTime + _samplingInterval;
// Iterate over active resamplers and sample pointer events for // Iterate over active resamplers and sample pointer events for
// current sample time. // current sample time.
...@@ -106,23 +144,30 @@ class _Resampler { ...@@ -106,23 +144,30 @@ class _Resampler {
// Save last sample time for debugPrint of resampling margin. // Save last sample time for debugPrint of resampling margin.
_lastSampleTime = sampleTime; _lastSampleTime = sampleTime;
// Early out if another call to `sample` isn't needed.
if (_resamplers.isEmpty) {
_timer!.cancel();
return;
}
// Schedule a frame callback if another call to `sample` is needed. // Schedule a frame callback if another call to `sample` is needed.
if (!_frameCallbackScheduled && _resamplers.isNotEmpty) { if (!_frameCallbackScheduled) {
_frameCallbackScheduled = true; _frameCallbackScheduled = true;
scheduler?.scheduleFrameCallback((_) { // Add a post frame callback as this avoids producing unnecessary
// frames but ensures that sampling phase is adjusted to frame
// time when frames are produced.
scheduler?.addPostFrameCallback((_) {
_frameCallbackScheduled = false; _frameCallbackScheduled = false;
// We use `currentSystemFrameTimeStamp` here as it's critical that // We use `currentSystemFrameTimeStamp` here as it's critical that
// sample time is in the same clock as the event time stamps, and // sample time is in the same clock as the event time stamps, and
// never adjusted or scaled like `currentFrameTimeStamp`. // never adjusted or scaled like `currentFrameTimeStamp`.
_frameTime = scheduler.currentSystemFrameTimeStamp; _frameTime = scheduler.currentSystemFrameTimeStamp;
assert(() { _frameTimeAge.reset();
if (debugPrintResamplingMargin) { // Reset timer to match phase of latest frame callback.
final Duration resamplingMargin = _lastEventTime - _lastSampleTime; _timer?.cancel();
debugPrint('$resamplingMargin'); _timer = Timer.periodic(_samplingInterval, (_) => _onSampleTimeChanged());
} // Trigger an immediate sample time change.
return true; _onSampleTimeChanged();
}());
_handleSampleTimeChanged();
}); });
} }
} }
...@@ -133,6 +178,18 @@ class _Resampler { ...@@ -133,6 +178,18 @@ class _Resampler {
resampler.stop(_handlePointerEvent); resampler.stop(_handlePointerEvent);
} }
_resamplers.clear(); _resamplers.clear();
_frameTime = Duration.zero;
}
void _onSampleTimeChanged() {
assert(() {
if (debugPrintResamplingMargin) {
final Duration resamplingMargin = _lastEventTime - _lastSampleTime;
debugPrint('$resamplingMargin');
}
return true;
}());
_handleSampleTimeChanged();
} }
} }
...@@ -147,7 +204,8 @@ const Duration _defaultSamplingOffset = Duration(milliseconds: -38); ...@@ -147,7 +204,8 @@ const Duration _defaultSamplingOffset = Duration(milliseconds: -38);
// The sampling interval. // The sampling interval.
// //
// Sampling interval is used to determine the approximate time for subsequent // Sampling interval is used to determine the approximate time for subsequent
// sampling. This is used to decide if early processing of up and removed events // sampling. This is used to sample events when frame callbacks are not
// being received and decide if early processing of up and removed events
// is appropriate. 16667 us for 60hz sampling interval. // is appropriate. 16667 us for 60hz sampling interval.
const Duration _samplingInterval = Duration(microseconds: 16667); const Duration _samplingInterval = Duration(microseconds: 16667);
...@@ -270,7 +328,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -270,7 +328,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
if (resamplingEnabled) { if (resamplingEnabled) {
_resampler.addOrDispatch(event); _resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingInterval); _resampler.sample(samplingOffset, _samplingClock);
return; return;
} }
...@@ -398,10 +456,16 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -398,10 +456,16 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
_hitTests.clear(); _hitTests.clear();
} }
/// Overrides the sampling clock for debugging and testing.
///
/// This value is ignored in non-debug builds.
@protected
SamplingClock? get debugSamplingClock => null;
void _handleSampleTimeChanged() { void _handleSampleTimeChanged() {
if (!locked) { if (!locked) {
if (resamplingEnabled) { if (resamplingEnabled) {
_resampler.sample(samplingOffset, _samplingInterval); _resampler.sample(samplingOffset, _samplingClock);
} }
else { else {
_resampler.stop(); _resampler.stop();
...@@ -409,11 +473,23 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -409,11 +473,23 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
} }
} }
SamplingClock get _samplingClock {
SamplingClock value = SamplingClock();
assert(() {
final SamplingClock? debugValue = debugSamplingClock;
if (debugValue != null)
value = debugValue;
return true;
}());
return value;
}
// Resampler used to filter incoming pointer events when resampling // Resampler used to filter incoming pointer events when resampling
// is enabled. // is enabled.
late final _Resampler _resampler = _Resampler( late final _Resampler _resampler = _Resampler(
_handlePointerEventImmediately, _handlePointerEventImmediately,
_handleSampleTimeChanged, _handleSampleTimeChanged,
_samplingInterval,
); );
/// Enable pointer event resampling for touch devices by setting /// Enable pointer event resampling for touch devices by setting
......
...@@ -10,15 +10,35 @@ ...@@ -10,15 +10,35 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:clock/clock.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class TestResampleEventFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
@override
SamplingClock? get debugSamplingClock => TestSamplingClock(this.clock);
}
class TestSamplingClock implements SamplingClock {
TestSamplingClock(this._clock);
@override
DateTime now() => _clock.now();
@override
Stopwatch stopwatch() => _clock.stopwatch();
final Clock _clock;
}
void main() { void main() {
final TestWidgetsFlutterBinding binding = AutomatedTestWidgetsFlutterBinding(); final TestWidgetsFlutterBinding binding = TestResampleEventFlutterBinding();
testWidgets('PointerEvent resampling on a widget', (WidgetTester tester) async { testWidgets('PointerEvent resampling on a widget', (WidgetTester tester) async {
assert(WidgetsBinding.instance == binding); assert(WidgetsBinding.instance == binding);
Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch); Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch);
void requestFrame() => SchedulerBinding.instance!.scheduleFrameCallback((_) {});
final Duration epoch = currentTestFrameTime(); final Duration epoch = currentTestFrameTime();
final ui.PointerDataPacket packet = ui.PointerDataPacket( final ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[ data: <ui.PointerData>[
...@@ -30,37 +50,37 @@ void main() { ...@@ -30,37 +50,37 @@ void main() {
ui.PointerData( ui.PointerData(
change: ui.PointerChange.down, change: ui.PointerChange.down,
physicalX: 0.0, physicalX: 0.0,
timeStamp: epoch + const Duration(milliseconds: 0),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 15.0,
timeStamp: epoch + const Duration(milliseconds: 10), timeStamp: epoch + const Duration(milliseconds: 10),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 10.0, physicalX: 30.0,
timeStamp: epoch + const Duration(milliseconds: 20), timeStamp: epoch + const Duration(milliseconds: 20),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 20.0, physicalX: 45.0,
timeStamp: epoch + const Duration(milliseconds: 30), timeStamp: epoch + const Duration(milliseconds: 30),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 30.0, physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 40), timeStamp: epoch + const Duration(milliseconds: 40),
), ),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 50),
),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.up, change: ui.PointerChange.up,
physicalX: 40.0, physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 60), timeStamp: epoch + const Duration(milliseconds: 40),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.remove, change: ui.PointerChange.remove,
physicalX: 40.0, physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 70), timeStamp: epoch + const Duration(milliseconds: 40),
), ),
], ],
); );
...@@ -84,29 +104,31 @@ void main() { ...@@ -84,29 +104,31 @@ void main() {
ui.window.onPointerDataPacket!(packet); ui.window.onPointerDataPacket!(packet);
expect(events.length, 0); expect(events.length, 0);
await tester.pump(const Duration(milliseconds: 20)); requestFrame();
await tester.pump(const Duration(milliseconds: 10));
expect(events.length, 1); expect(events.length, 1);
expect(events[0], isA<PointerDownEvent>()); expect(events[0], isA<PointerDownEvent>());
expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0)); expect(events[0].position, Offset(7.5 / ui.window.devicePixelRatio, 0.0));
// Now the system time is epoch + 40ms // Now the system time is epoch + 20ms
await tester.pump(const Duration(milliseconds: 20)); requestFrame();
await tester.pump(const Duration(milliseconds: 10));
expect(events.length, 2); expect(events.length, 2);
expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[1], isA<PointerMoveEvent>()); expect(events[1], isA<PointerMoveEvent>());
expect(events[1].position, Offset(25.0 / ui.window.devicePixelRatio, 0.0)); expect(events[1].position, Offset(22.5 / ui.window.devicePixelRatio, 0.0));
expect(events[1].delta, Offset(20.0 / ui.window.devicePixelRatio, 0.0)); expect(events[1].delta, Offset(15.0 / ui.window.devicePixelRatio, 0.0));
// Now the system time is epoch + 60ms // Now the system time is epoch + 30ms
await tester.pump(const Duration(milliseconds: 20)); requestFrame();
await tester.pump(const Duration(milliseconds: 10));
expect(events.length, 4); expect(events.length, 4);
expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[2], isA<PointerMoveEvent>()); expect(events[2], isA<PointerMoveEvent>());
expect(events[2].position, Offset(40.0 / ui.window.devicePixelRatio, 0.0)); expect(events[2].position, Offset(37.5 / ui.window.devicePixelRatio, 0.0));
expect(events[2].delta, Offset(15.0 / ui.window.devicePixelRatio, 0.0)); expect(events[2].delta, Offset(15.0 / ui.window.devicePixelRatio, 0.0));
expect(events[3].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[3].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[3], isA<PointerUpEvent>()); expect(events[3], isA<PointerUpEvent>());
expect(events[3].position, Offset(40.0 / ui.window.devicePixelRatio, 0.0));
}); });
} }
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:clock/clock.dart';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import '../flutter_test_alternative.dart';
typedef HandleEventCallback = void Function(PointerEvent event);
class TestResampleEventFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding {
HandleEventCallback? callback;
FrameCallback? postFrameCallback;
Duration? frameTime;
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
super.handleEvent(event, entry);
if (callback != null)
callback?.call(event);
}
@override
Duration get currentSystemFrameTimeStamp {
assert(frameTime != null);
return frameTime!;
}
@override
int addPostFrameCallback(FrameCallback callback) {
postFrameCallback = callback;
return 0;
}
@override
SamplingClock? get debugSamplingClock => TestSamplingClock();
}
class TestSamplingClock implements SamplingClock {
@override
DateTime now() => clock.now();
@override
Stopwatch stopwatch() => clock.stopwatch();
}
typedef ResampleEventTest = void Function(FakeAsync async);
void testResampleEvent(String description, ResampleEventTest callback) {
test(description, () {
fakeAsync((FakeAsync async) {
callback(async);
}, initialTime: DateTime.utc(2015, 1, 1));
}, skip: isBrowser); // Fake clock is not working with the web platform.
}
void main() {
final TestResampleEventFlutterBinding binding = TestResampleEventFlutterBinding();
testResampleEvent('Pointer event resampling', (FakeAsync async) {
Duration currentTime() => Duration(milliseconds: clock.now().millisecondsSinceEpoch);
final Duration epoch = currentTime();
final ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.add,
physicalX: 0.0,
timeStamp: epoch + const Duration(milliseconds: 0),
),
ui.PointerData(
change: ui.PointerChange.down,
physicalX: 0.0,
timeStamp: epoch + const Duration(milliseconds: 10),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 10.0,
timeStamp: epoch + const Duration(milliseconds: 20),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 20.0,
timeStamp: epoch + const Duration(milliseconds: 30),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 30.0,
timeStamp: epoch + const Duration(milliseconds: 40),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 50),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 60),
),
ui.PointerData(
change: ui.PointerChange.up,
physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 70),
),
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 70),
),
],
);
const Duration samplingOffset = Duration(milliseconds: -5);
const Duration frameInterval = Duration(microseconds: 16667);
GestureBinding.instance!.resamplingEnabled = true;
GestureBinding.instance!.samplingOffset = samplingOffset;
final List<PointerEvent> events = <PointerEvent>[];
binding.callback = events.add;
ui.window.onPointerDataPacket?.call(packet);
// No pointer events should have been dispatched yet.
expect(events.length, 0);
// Frame callback should have been requested.
FrameCallback? callback = binding.postFrameCallback;
binding.postFrameCallback = null;
expect(callback, isNotNull);
binding.frameTime = epoch + const Duration(milliseconds: 15);
callback!(Duration.zero);
// One pointer event should have been dispatched.
expect(events.length, 1);
expect(events[0], isA<PointerDownEvent>());
expect(events[0].timeStamp, binding.frameTime! + samplingOffset);
expect(events[0].position, Offset(0.0 / ui.window.devicePixelRatio, 0.0));
// Second frame callback should have been requested.
callback = binding.postFrameCallback;
binding.postFrameCallback = null;
expect(callback, isNotNull);
final Duration frameTime = epoch + const Duration(milliseconds: 25);
binding.frameTime = frameTime;
callback!(Duration.zero);
// Second pointer event should have been dispatched.
expect(events.length, 2);
expect(events[1], isA<PointerMoveEvent>());
expect(events[1].timeStamp, binding.frameTime! + samplingOffset);
expect(events[1].position, Offset(10.0 / ui.window.devicePixelRatio, 0.0));
expect(events[1].delta, Offset(10.0 / ui.window.devicePixelRatio, 0.0));
// Verify that resampling continues without a frame callback.
async.elapse(frameInterval * 1.5);
// Third pointer event should have been dispatched.
expect(events.length, 3);
expect(events[2], isA<PointerMoveEvent>());
expect(events[2].timeStamp, frameTime + frameInterval + samplingOffset);
async.elapse(frameInterval);
// Remaining pointer events should have been dispatched.
expect(events.length, 5);
expect(events[3], isA<PointerMoveEvent>());
expect(events[3].timeStamp, frameTime + frameInterval * 2 + samplingOffset);
expect(events[4], isA<PointerUpEvent>());
expect(events[4].timeStamp, frameTime + frameInterval * 2 + samplingOffset);
async.elapse(frameInterval);
// No more pointer events should have been dispatched.
expect(events.length, 5);
GestureBinding.instance!.resamplingEnabled = false;
});
}
...@@ -14,8 +14,6 @@ typedef HandleEventCallback = void Function(PointerEvent event); ...@@ -14,8 +14,6 @@ typedef HandleEventCallback = void Function(PointerEvent event);
class TestGestureFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding { class TestGestureFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding {
HandleEventCallback? callback; HandleEventCallback? callback;
FrameCallback? frameCallback;
Duration? frameTime;
@override @override
void handleEvent(PointerEvent event, HitTestEntry entry) { void handleEvent(PointerEvent event, HitTestEntry entry) {
...@@ -23,18 +21,6 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding, Schedul ...@@ -23,18 +21,6 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding, Schedul
if (callback != null) if (callback != null)
callback?.call(event); callback?.call(event);
} }
@override
Duration get currentSystemFrameTimeStamp {
assert(frameTime != null);
return frameTime!;
}
@override
int scheduleFrameCallback(FrameCallback callback, {bool rescheduling = false}) {
frameCallback = callback;
return 0;
}
} }
TestGestureFlutterBinding? _binding; TestGestureFlutterBinding? _binding;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment