diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 5a460b79aca..7aa5f08f6dd 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -21,24 +21,13 @@ import 'resampler.dart'; 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 // devices. // -// The `samplingInterval` is used to determine the approximate next -// time for resampling. // SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine // sample time. class _Resampler { - _Resampler(this._handlePointerEvent, this._handleSampleTimeChanged, this._samplingInterval); + _Resampler(this._handlePointerEvent, this._handleSampleTimeChanged); // Resamplers used to filter incoming pointer events. final Map _resamplers = {}; @@ -46,12 +35,9 @@ class _Resampler { // Flag to track if a frame callback has been scheduled. bool _frameCallbackScheduled = false; - // Last frame time for resampling. + // Current frame time for resampling. Duration _frameTime = Duration.zero; - // Time since `_frameTime` was updated. - Stopwatch _frameTimeAge = Stopwatch(); - // Last sample time and time stamp of last event. // // Only used for debugPrint of resampling margin. @@ -64,18 +50,12 @@ class _Resampler { // Callback used to handle sample time changes. 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 // not a touch event. void addOrDispatch(PointerEvent event) { final SchedulerBinding? scheduler = SchedulerBinding.instance; 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) { // Save last event time for debugPrint of resampling margin. _lastEventTime = event.timeStamp; @@ -92,43 +72,25 @@ class _Resampler { // Sample and dispatch events. // - // The `samplingOffset` is relative to the current frame time, which + // `samplingOffset` is relative to the current frame time, which // can be in the past when we're not actively resampling. - // The `samplingClock` is the clock used to determine frame time age. - void sample(Duration samplingOffset, SamplingClock clock) { + // `samplingInterval` is used to determine the approximate next + // time for resampling. + // `currentSystemFrameTimeStamp` is used to determine the current + // frame time. + void sample(Duration samplingOffset, Duration samplingInterval) { final SchedulerBinding? scheduler = SchedulerBinding.instance; 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 // frame time. This is expected to be in the past and not // result in any dispatched events unless we're actively // resampling events. - final Duration sampleTime = frameTime + samplingOffset; + final Duration sampleTime = _frameTime + samplingOffset; // Determine next sample time by adding the sampling interval // to the current sample time. - final Duration nextSampleTime = sampleTime + _samplingInterval; + final Duration nextSampleTime = sampleTime + samplingInterval; // Iterate over active resamplers and sample pointer events for // current sample time. @@ -144,30 +106,23 @@ class _Resampler { // Save last sample time for debugPrint of resampling margin. _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. - if (!_frameCallbackScheduled) { + if (!_frameCallbackScheduled && _resamplers.isNotEmpty) { _frameCallbackScheduled = true; - // 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((_) { + scheduler?.scheduleFrameCallback((_) { _frameCallbackScheduled = false; // We use `currentSystemFrameTimeStamp` here as it's critical that // sample time is in the same clock as the event time stamps, and // never adjusted or scaled like `currentFrameTimeStamp`. _frameTime = scheduler.currentSystemFrameTimeStamp; - _frameTimeAge.reset(); - // Reset timer to match phase of latest frame callback. - _timer?.cancel(); - _timer = Timer.periodic(_samplingInterval, (_) => _onSampleTimeChanged()); - // Trigger an immediate sample time change. - _onSampleTimeChanged(); + assert(() { + if (debugPrintResamplingMargin) { + final Duration resamplingMargin = _lastEventTime - _lastSampleTime; + debugPrint('$resamplingMargin'); + } + return true; + }()); + _handleSampleTimeChanged(); }); } } @@ -178,18 +133,6 @@ class _Resampler { resampler.stop(_handlePointerEvent); } _resamplers.clear(); - _frameTime = Duration.zero; - } - - void _onSampleTimeChanged() { - assert(() { - if (debugPrintResamplingMargin) { - final Duration resamplingMargin = _lastEventTime - _lastSampleTime; - debugPrint('$resamplingMargin'); - } - return true; - }()); - _handleSampleTimeChanged(); } } @@ -204,8 +147,7 @@ const Duration _defaultSamplingOffset = Duration(milliseconds: -38); // The sampling interval. // // Sampling interval is used to determine the approximate time for subsequent -// sampling. This is used to sample events when frame callbacks are not -// being received and decide if early processing of up and removed events +// sampling. This is used to decide if early processing of up and removed events // is appropriate. 16667 us for 60hz sampling interval. const Duration _samplingInterval = Duration(microseconds: 16667); @@ -328,7 +270,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H if (resamplingEnabled) { _resampler.addOrDispatch(event); - _resampler.sample(samplingOffset, _samplingClock); + _resampler.sample(samplingOffset, _samplingInterval); return; } @@ -456,16 +398,10 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H _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() { if (!locked) { if (resamplingEnabled) { - _resampler.sample(samplingOffset, _samplingClock); + _resampler.sample(samplingOffset, _samplingInterval); } else { _resampler.stop(); @@ -473,23 +409,11 @@ 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 // is enabled. late final _Resampler _resampler = _Resampler( _handlePointerEventImmediately, _handleSampleTimeChanged, - _samplingInterval, ); /// Enable pointer event resampling for touch devices by setting diff --git a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart index 3c3e397706b..bea0c48db27 100644 --- a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart @@ -10,35 +10,15 @@ import 'dart:ui' as ui; -import 'package:clock/clock.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.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() { - final TestWidgetsFlutterBinding binding = TestResampleEventFlutterBinding(); + final TestWidgetsFlutterBinding binding = AutomatedTestWidgetsFlutterBinding(); testWidgets('PointerEvent resampling on a widget', (WidgetTester tester) async { assert(WidgetsBinding.instance == binding); Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch); - void requestFrame() => SchedulerBinding.instance!.scheduleFrameCallback((_) {}); final Duration epoch = currentTestFrameTime(); final ui.PointerDataPacket packet = ui.PointerDataPacket( data: [ @@ -50,37 +30,37 @@ void main() { ui.PointerData( change: ui.PointerChange.down, physicalX: 0.0, - timeStamp: epoch + const Duration(milliseconds: 0), - ), - ui.PointerData( - change: ui.PointerChange.move, - physicalX: 15.0, timeStamp: epoch + const Duration(milliseconds: 10), ), ui.PointerData( change: ui.PointerChange.move, - physicalX: 30.0, + physicalX: 10.0, timeStamp: epoch + const Duration(milliseconds: 20), ), ui.PointerData( change: ui.PointerChange.move, - physicalX: 45.0, + physicalX: 20.0, timeStamp: epoch + const Duration(milliseconds: 30), ), ui.PointerData( change: ui.PointerChange.move, - physicalX: 50.0, + 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.up, - physicalX: 60.0, - timeStamp: epoch + const Duration(milliseconds: 40), + physicalX: 40.0, + timeStamp: epoch + const Duration(milliseconds: 60), ), ui.PointerData( change: ui.PointerChange.remove, - physicalX: 60.0, - timeStamp: epoch + const Duration(milliseconds: 40), + physicalX: 40.0, + timeStamp: epoch + const Duration(milliseconds: 70), ), ], ); @@ -104,31 +84,29 @@ void main() { ui.window.onPointerDataPacket!(packet); expect(events.length, 0); - requestFrame(); - await tester.pump(const Duration(milliseconds: 10)); + await tester.pump(const Duration(milliseconds: 20)); expect(events.length, 1); expect(events[0], isA()); expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset); - expect(events[0].position, Offset(7.5 / ui.window.devicePixelRatio, 0.0)); + expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0)); - // Now the system time is epoch + 20ms - requestFrame(); - await tester.pump(const Duration(milliseconds: 10)); + // Now the system time is epoch + 40ms + await tester.pump(const Duration(milliseconds: 20)); expect(events.length, 2); expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[1], isA()); - expect(events[1].position, Offset(22.5 / ui.window.devicePixelRatio, 0.0)); - expect(events[1].delta, Offset(15.0 / ui.window.devicePixelRatio, 0.0)); + expect(events[1].position, Offset(25.0 / ui.window.devicePixelRatio, 0.0)); + expect(events[1].delta, Offset(20.0 / ui.window.devicePixelRatio, 0.0)); - // Now the system time is epoch + 30ms - requestFrame(); - await tester.pump(const Duration(milliseconds: 10)); + // Now the system time is epoch + 60ms + await tester.pump(const Duration(milliseconds: 20)); expect(events.length, 4); expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[2], isA()); - expect(events[2].position, Offset(37.5 / ui.window.devicePixelRatio, 0.0)); + expect(events[2].position, Offset(40.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], isA()); + expect(events[3].position, Offset(40.0 / ui.window.devicePixelRatio, 0.0)); }); } diff --git a/packages/flutter/test/gestures/gesture_binding_resample_event_test.dart b/packages/flutter/test/gestures/gesture_binding_resample_event_test.dart deleted file mode 100644 index 492af5fca11..00000000000 --- a/packages/flutter/test/gestures/gesture_binding_resample_event_test.dart +++ /dev/null @@ -1,186 +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( - 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 events = []; - 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()); - 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()); - 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()); - 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()); - expect(events[3].timeStamp, frameTime + frameInterval * 2 + samplingOffset); - expect(events[4], isA()); - 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; - }); -} diff --git a/packages/flutter/test/gestures/gesture_binding_test.dart b/packages/flutter/test/gestures/gesture_binding_test.dart index 9a446f12f24..b90bcb2b867 100644 --- a/packages/flutter/test/gestures/gesture_binding_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_test.dart @@ -14,6 +14,8 @@ typedef HandleEventCallback = void Function(PointerEvent event); class TestGestureFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding { HandleEventCallback? callback; + FrameCallback? frameCallback; + Duration? frameTime; @override void handleEvent(PointerEvent event, HitTestEntry entry) { @@ -21,6 +23,18 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding, Schedul if (callback != null) 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;