// 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:collection'; import 'events.dart'; export 'events.dart' show PointerEvent; /// A callback used by [PointerEventResampler.sample] and /// [PointerEventResampler.stop] to process a resampled `event`. typedef HandleEventCallback = void Function(PointerEvent event); /// Class for pointer event resampling. /// /// An instance of this class can be used to resample one sequence /// of pointer events. Multiple instances are expected to be used for /// multi-touch support. The sampling frequency and the sampling /// offset is determined by the caller. /// /// This can be used to get smooth touch event processing at the cost /// of adding some 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. /// /// The following pointer event types are supported: /// [PointerAddedEvent], [PointerHoverEvent], [PointerDownEvent], /// [PointerMoveEvent], [PointerCancelEvent], [PointerUpEvent], /// [PointerRemovedEvent]. /// /// Resampling is currently limited to event position and delta. All /// pointer event types except [PointerAddedEvent] will be resampled. /// [PointerHoverEvent] and [PointerMoveEvent] will only be generated /// when the position has changed. class PointerEventResampler { // Events queued for processing. final Queue<PointerEvent> _queuedEvents = Queue<PointerEvent>(); // Pointer state required for resampling. PointerEvent? _last; PointerEvent? _next; Offset _position = Offset.zero; bool _isTracked = false; bool _isDown = false; int _pointerIdentifier = 0; int _hasButtons = 0; PointerEvent _toHoverEvent( PointerEvent event, Offset position, Offset delta, Duration timeStamp, int buttons, ) { return PointerHoverEvent( viewId: event.viewId, timeStamp: timeStamp, kind: event.kind, device: event.device, position: position, delta: delta, buttons: event.buttons, obscured: event.obscured, pressureMin: event.pressureMin, pressureMax: event.pressureMax, distance: event.distance, distanceMax: event.distanceMax, size: event.size, radiusMajor: event.radiusMajor, radiusMinor: event.radiusMinor, radiusMin: event.radiusMin, radiusMax: event.radiusMax, orientation: event.orientation, tilt: event.tilt, synthesized: event.synthesized, embedderId: event.embedderId, ); } PointerEvent _toMoveEvent( PointerEvent event, Offset position, Offset delta, int pointerIdentifier, Duration timeStamp, int buttons, ) { return PointerMoveEvent( viewId: event.viewId, timeStamp: timeStamp, pointer: pointerIdentifier, kind: event.kind, device: event.device, position: position, delta: delta, buttons: buttons, obscured: event.obscured, pressure: event.pressure, pressureMin: event.pressureMin, pressureMax: event.pressureMax, distanceMax: event.distanceMax, size: event.size, radiusMajor: event.radiusMajor, radiusMinor: event.radiusMinor, radiusMin: event.radiusMin, radiusMax: event.radiusMax, orientation: event.orientation, tilt: event.tilt, platformData: event.platformData, synthesized: event.synthesized, embedderId: event.embedderId, ); } PointerEvent _toMoveOrHoverEvent( PointerEvent event, Offset position, Offset delta, int pointerIdentifier, Duration timeStamp, bool isDown, int buttons, ) { return isDown ? _toMoveEvent( event, position, delta, pointerIdentifier, timeStamp, buttons) : _toHoverEvent(event, position, delta, timeStamp, buttons); } Offset _positionAt(Duration sampleTime) { // Use `next` position by default. double x = _next?.position.dx ?? 0.0; double y = _next?.position.dy ?? 0.0; final Duration nextTimeStamp = _next?.timeStamp ?? Duration.zero; final Duration lastTimeStamp = _last?.timeStamp ?? Duration.zero; // Resample if `next` time stamp is past `sampleTime`. if (nextTimeStamp > sampleTime && nextTimeStamp > lastTimeStamp) { final double interval = (nextTimeStamp - lastTimeStamp).inMicroseconds.toDouble(); final double scalar = (sampleTime - lastTimeStamp).inMicroseconds.toDouble() / interval; final double lastX = _last?.position.dx ?? 0.0; final double lastY = _last?.position.dy ?? 0.0; x = lastX + (x - lastX) * scalar; y = lastY + (y - lastY) * scalar; } return Offset(x, y); } void _processPointerEvents(Duration sampleTime) { final Iterator<PointerEvent> it = _queuedEvents.iterator; while (it.moveNext()) { final PointerEvent event = it.current; // Update both `last` and `next` pointer event if time stamp is older // or equal to `sampleTime`. if (event.timeStamp <= sampleTime || _last == null) { _last = event; _next = event; continue; } // Update only `next` pointer event if time stamp is more recent than // `sampleTime` and next event is not already more recent. final Duration nextTimeStamp = _next?.timeStamp ?? Duration.zero; if (nextTimeStamp < sampleTime) { _next = event; break; } } } void _dequeueAndSampleNonHoverOrMovePointerEventsUntil( Duration sampleTime, Duration nextSampleTime, HandleEventCallback callback, ) { Duration endTime = sampleTime; // Scan queued events to determine end time. final Iterator<PointerEvent> it = _queuedEvents.iterator; while (it.moveNext()) { final PointerEvent event = it.current; // Potentially stop dispatching events if more recent than `sampleTime`. if (event.timeStamp > sampleTime) { // Definitely stop if more recent than `nextSampleTime`. if (event.timeStamp >= nextSampleTime) { break; } // Update `endTime` to allow early processing of up and removed // events as this improves resampling of these events, which is // important for fling animations. if (event is PointerUpEvent || event is PointerRemovedEvent) { endTime = event.timeStamp; continue; } // Stop if event is not move or hover. if (event is! PointerMoveEvent && event is! PointerHoverEvent) { break; } } } while (_queuedEvents.isNotEmpty) { final PointerEvent event = _queuedEvents.first; // Stop dispatching events if more recent than `endTime`. if (event.timeStamp > endTime) { break; } final bool wasTracked = _isTracked; final bool wasDown = _isDown; final int hadButtons = _hasButtons; // Update pointer state. _isTracked = event is! PointerRemovedEvent; _isDown = event.down; _hasButtons = event.buttons; // Position at `sampleTime`. final Offset position = _positionAt(sampleTime); // Initialize position if we are starting to track this pointer. if (_isTracked && !wasTracked) { _position = position; } // Current pointer identifier. final int pointerIdentifier = event.pointer; // Initialize pointer identifier for `move` events. // Identifier is expected to be the same while `down`. assert(!wasDown || _pointerIdentifier == pointerIdentifier); _pointerIdentifier = pointerIdentifier; // Skip `move` and `hover` events as they are automatically // generated when the position has changed. if (event is! PointerMoveEvent && event is! PointerHoverEvent) { // Add synthetics `move` or `hover` event if position has changed. // // Devices without `hover` events are expected to always have // `add` and `down` events with the same position and this logic will // therefore never produce `hover` events. if (position != _position) { final Offset delta = position - _position; callback(_toMoveOrHoverEvent(event, position, delta, _pointerIdentifier, sampleTime, wasDown, hadButtons)); _position = position; } callback(event.copyWith( position: position, delta: Offset.zero, pointer: pointerIdentifier, timeStamp: sampleTime, )); } _queuedEvents.removeFirst(); } } void _samplePointerPosition( Duration sampleTime, HandleEventCallback callback, ) { // Position at `sampleTime`. final Offset position = _positionAt(sampleTime); // Add `move` or `hover` events if position has changed. final PointerEvent? next = _next; if (position != _position && next != null) { final Offset delta = position - _position; callback(_toMoveOrHoverEvent(next, position, delta, _pointerIdentifier, sampleTime, _isDown, _hasButtons)); _position = position; } } /// Enqueue pointer `event` for resampling. void addEvent(PointerEvent event) { _queuedEvents.add(event); } /// Dispatch resampled pointer events for the specified `sampleTime` /// by calling [callback]. /// /// This may dispatch multiple events if position is not the only /// state that has changed since last sample. /// /// Calling [callback] must not add or sample events. /// /// Positive value for `nextSampleTime` allow early processing of /// up and removed events. This improves resampling of these events, /// which is important for fling animations. void sample( Duration sampleTime, Duration nextSampleTime, HandleEventCallback callback, ) { _processPointerEvents(sampleTime); // Dequeue and sample pointer events until `sampleTime`. _dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, nextSampleTime, callback); // Dispatch resampled pointer location event if tracked. if (_isTracked) { _samplePointerPosition(sampleTime, callback); } } /// Stop resampling. /// /// This will dispatch pending events by calling [callback] and reset /// internal state. void stop(HandleEventCallback callback) { while (_queuedEvents.isNotEmpty) { callback(_queuedEvents.removeFirst()); } _pointerIdentifier = 0; _isDown = false; _isTracked = false; _position = Offset.zero; _next = null; _last = null; } /// Returns `true` if a call to [sample] can dispatch more events. bool get hasPendingEvents => _queuedEvents.isNotEmpty; /// Returns `true` if pointer is currently tracked. bool get isTracked => _isTracked; /// Returns `true` if pointer is currently down. bool get isDown => _isDown; }