// 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:async'; import 'dart:collection'; import 'dart:ui' as ui show PointerDataPacket; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'arena.dart'; import 'converter.dart'; import 'debug.dart'; import 'events.dart'; import 'hit_test.dart'; import 'pointer_router.dart'; import 'pointer_signal_resolver.dart'; 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); // Resamplers used to filter incoming pointer events. final Map<int, PointerEventResampler> _resamplers = <int, PointerEventResampler>{}; // Flag to track if a frame callback has been scheduled. bool _frameCallbackScheduled = false; // Last 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. Duration _lastSampleTime = Duration.zero; Duration _lastEventTime = Duration.zero; // Callback used to handle pointer events. final HandleEventCallback _handlePointerEvent; // 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. if (event.kind == PointerDeviceKind.touch) { // Save last event time for debugPrint of resampling margin. _lastEventTime = event.timeStamp; final PointerEventResampler resampler = _resamplers.putIfAbsent( event.device, () => PointerEventResampler(), ); resampler.addEvent(event); } else { _handlePointerEvent(event); } } // Sample and dispatch events. // // The `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) { 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 != true) { _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; // Determine next sample time by adding the sampling interval // to the current sample time. final Duration nextSampleTime = sampleTime + _samplingInterval; // Iterate over active resamplers and sample pointer events for // current sample time. for (final PointerEventResampler resampler in _resamplers.values) { resampler.sample(sampleTime, nextSampleTime, _handlePointerEvent); } // Remove inactive resamplers. _resamplers.removeWhere((int key, PointerEventResampler resampler) { return !resampler.hasPendingEvents && !resampler.isDown; }); // 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) { _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((_) { _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(); }); } } // Stop all resampling and dispatched any queued events. void stop() { for (final PointerEventResampler resampler in _resamplers.values) { resampler.stop(_handlePointerEvent); } _resamplers.clear(); _frameTime = Duration.zero; } void _onSampleTimeChanged() { assert(() { if (debugPrintResamplingMargin) { final Duration resamplingMargin = _lastEventTime - _lastSampleTime; debugPrint('$resamplingMargin'); } return true; }()); _handleSampleTimeChanged(); } } // The default sampling offset. // // Sampling offset is relative to presentation time. If we produce frames // 16.667 ms before presentation and input rate is ~60hz, worst case latency // is 33.334 ms. This however assumes zero latency from the input driver. // 4.666 ms margin is added for this. 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 // is appropriate. 16667 us for 60hz sampling interval. const Duration _samplingInterval = Duration(microseconds: 16667); /// A binding for the gesture subsystem. /// /// ## Lifecycle of pointer events and the gesture arena /// /// ### [PointerDownEvent] /// /// When a [PointerDownEvent] is received by the [GestureBinding] (from /// [dart:ui.PlatformDispatcher.onPointerDataPacket], as interpreted by the /// [PointerEventConverter]), a [hitTest] is performed to determine which /// [HitTestTarget] nodes are affected. (Other bindings are expected to /// implement [hitTest] to defer to [HitTestable] objects. For example, the /// rendering layer defers to the [RenderView] and the rest of the render object /// hierarchy.) /// /// The affected nodes then are given the event to handle ([dispatchEvent] calls /// [HitTestTarget.handleEvent] for each affected node). If any have relevant /// [GestureRecognizer]s, they provide the event to them using /// [GestureRecognizer.addPointer]. This typically causes the recognizer to /// register with the [PointerRouter] to receive notifications regarding the /// pointer in question. /// /// Once the hit test and dispatching logic is complete, the event is then /// passed to the aforementioned [PointerRouter], which passes it to any objects /// that have registered interest in that event. /// /// Finally, the [gestureArena] is closed for the given pointer /// ([GestureArenaManager.close]), which begins the process of selecting a /// gesture to win that pointer. /// /// ### Other events /// /// A pointer that is [PointerEvent.down] may send further events, such as /// [PointerMoveEvent], [PointerUpEvent], or [PointerCancelEvent]. These are /// sent to the same [HitTestTarget] nodes as were found when the /// [PointerDownEvent] was received (even if they have since been disposed; it is /// the responsibility of those objects to be aware of that possibility). /// /// Then, the events are routed to any still-registered entrants in the /// [PointerRouter]'s table for that pointer. /// /// When a [PointerUpEvent] is received, the [GestureArenaManager.sweep] method /// is invoked to force the gesture arena logic to terminate if necessary. mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget { @override void initInstances() { super.initInstances(); _instance = this; window.onPointerDataPacket = _handlePointerDataPacket; } @override void unlocked() { super.unlocked(); _flushPointerEventQueue(); } /// The singleton instance of this object. static GestureBinding? get instance => _instance; static GestureBinding? _instance; final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>(); void _handlePointerDataPacket(ui.PointerDataPacket packet) { // We convert pointer data to logical pixels so that e.g. the touch slop can be // defined in a device-independent manner. _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio)); if (!locked) _flushPointerEventQueue(); } /// Dispatch a [PointerCancelEvent] for the given pointer soon. /// /// The pointer event will be dispatched before the next pointer event and /// before the end of the microtask but not within this function call. void cancelPointer(int pointer) { if (_pendingPointerEvents.isEmpty && !locked) scheduleMicrotask(_flushPointerEventQueue); _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer)); } void _flushPointerEventQueue() { assert(!locked); while (_pendingPointerEvents.isNotEmpty) handlePointerEvent(_pendingPointerEvents.removeFirst()); } /// A router that routes all pointer events received from the engine. final PointerRouter pointerRouter = PointerRouter(); /// The gesture arenas used for disambiguating the meaning of sequences of /// pointer events. final GestureArenaManager gestureArena = GestureArenaManager(); /// The resolver used for determining which widget handles a /// [PointerSignalEvent]. final PointerSignalResolver pointerSignalResolver = PointerSignalResolver(); /// State for all pointers which are currently down. /// /// The state of hovering pointers is not tracked because that would require /// hit-testing on every frame. final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{}; /// Dispatch an event to the targets found by a hit test on its position. /// /// This method sends the given event to [dispatchEvent] based on event types: /// /// * [PointerDownEvent]s and [PointerSignalEvent]s are dispatched to the /// result of a new [hitTest]. /// * [PointerUpEvent]s and [PointerMoveEvent]s are dispatched to the result of hit test of the /// preceding [PointerDownEvent]s. /// * [PointerHoverEvent]s, [PointerAddedEvent]s, and [PointerRemovedEvent]s /// are dispatched without a hit test result. void handlePointerEvent(PointerEvent event) { assert(!locked); if (resamplingEnabled) { _resampler.addOrDispatch(event); _resampler.sample(samplingOffset, _samplingClock); return; } // Stop resampler if resampling is not enabled. This is a no-op if // resampling was never enabled. _resampler.stop(); _handlePointerEventImmediately(event); } void _handlePointerEventImmediately(PointerEvent event) { HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { assert(!_hitTests.containsKey(event.pointer)); hitTestResult = HitTestResult(); hitTest(hitTestResult, event.position); if (event is PointerDownEvent) { _hitTests[event.pointer] = hitTestResult; } assert(() { if (debugPrintHitTestResults) debugPrint('$event: $hitTestResult'); return true; }()); } else if (event is PointerUpEvent || event is PointerCancelEvent) { hitTestResult = _hitTests.remove(event.pointer); } else if (event.down) { // Because events that occur with the pointer down (like // [PointerMoveEvent]s) should be dispatched to the same place that their // initial PointerDownEvent was, we want to re-use the path we found when // the pointer went down, rather than do hit detection each time we get // such an event. hitTestResult = _hitTests[event.pointer]; } assert(() { if (debugPrintMouseHoverEvents && event is PointerHoverEvent) debugPrint('$event'); return true; }()); if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); dispatchEvent(event, hitTestResult); } } /// Determine which [HitTestTarget] objects are located at a given position. @override // from HitTestable void hitTest(HitTestResult result, Offset position) { result.add(HitTestEntry(this)); } /// Dispatch an event to [pointerRouter] and the path of a hit test result. /// /// The `event` is routed to [pointerRouter]. If the `hitTestResult` is not /// null, the event is also sent to every [HitTestTarget] in the entries of the /// given [HitTestResult]. Any exceptions from the handlers are caught. /// /// The `hitTestResult` argument may only be null for [PointerAddedEvent]s or /// [PointerRemovedEvent]s. @override // from HitTestDispatcher @pragma('vm:notify-debugger-on-exception') void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); // No hit test information implies that this is a [PointerHoverEvent], // [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially // routed here; other events will be routed through the `handleEvent` below. if (hitTestResult == null) { assert(event is PointerAddedEvent || event is PointerRemovedEvent); try { pointerRouter.route(event); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher( exception: exception, stack: stack, library: 'gesture library', context: ErrorDescription('while dispatching a non-hit-tested pointer event'), event: event, informationCollector: () sync* { yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty); }, )); } return; } for (final HitTestEntry entry in hitTestResult.path) { try { entry.target.handleEvent(event.transformed(entry.transform), entry); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher( exception: exception, stack: stack, library: 'gesture library', context: ErrorDescription('while dispatching a pointer event'), event: event, hitTestEntry: entry, informationCollector: () sync* { yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty); yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty); }, )); } } } @override // from HitTestTarget void handleEvent(PointerEvent event, HitTestEntry entry) { pointerRouter.route(event); if (event is PointerDownEvent) { gestureArena.close(event.pointer); } else if (event is PointerUpEvent) { gestureArena.sweep(event.pointer); } else if (event is PointerSignalEvent) { pointerSignalResolver.resolve(event); } } /// Reset states of [GestureBinding]. /// /// This clears the hit test records. /// /// This is typically called between tests. @protected void resetGestureBinding() { _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); } else { _resampler.stop(); } } } 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 /// this to true. /// /// Resampling results in smoother touch event processing at the /// cost of some added latency. Devices with low frequency sensors /// or when the frequency is not a multiple of the display frequency /// (e.g., 120Hz input and 90Hz display) benefit from this. /// /// This is typically set during application initialization but /// can be adjusted dynamically in case the application only /// wants resampling for some period of time. bool resamplingEnabled = false; /// Offset relative to current frame time that should be used for /// resampling. The [samplingOffset] is expected to be negative. /// Non-negative [samplingOffset] is allowed but will effectively /// disable resampling. Duration samplingOffset = _defaultSamplingOffset; } /// Variant of [FlutterErrorDetails] with extra fields for the gesture /// library's binding's pointer event dispatcher ([GestureBinding.dispatchEvent]). class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails { /// Creates a [FlutterErrorDetailsForPointerEventDispatcher] object with the given /// arguments setting the object's properties. /// /// The gesture library calls this constructor when catching an exception /// that will subsequently be reported using [FlutterError.onError]. const FlutterErrorDetailsForPointerEventDispatcher({ required Object exception, StackTrace? stack, String? library, DiagnosticsNode? context, this.event, this.hitTestEntry, InformationCollector? informationCollector, bool silent = false, }) : super( exception: exception, stack: stack, library: library, context: context, informationCollector: informationCollector, silent: silent, ); /// The pointer event that was being routed when the exception was raised. final PointerEvent? event; /// The hit test result entry for the object whose handleEvent method threw /// the exception. May be null if no hit test entry is associated with the /// event (e.g. [PointerHoverEvent]s, [PointerAddedEvent]s, and /// [PointerRemovedEvent]s). /// /// The target object itself is given by the [HitTestEntry.target] property of /// the hitTestEntry object. final HitTestEntry? hitTestEntry; }