| // 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'; |
| |
| /// 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; |
| |
| PointerEvent _toHoverEvent( |
| PointerEvent event, |
| Offset position, |
| Offset delta, |
| Duration timeStamp, |
| ) { |
| return PointerHoverEvent( |
| 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, |
| ) { |
| return PointerMoveEvent( |
| timeStamp: timeStamp, |
| pointer: pointerIdentifier, |
| kind: event.kind, |
| device: event.device, |
| position: position, |
| delta: delta, |
| buttons: event.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, |
| ) { |
| return isDown ? _toMoveEvent(event, position, delta, pointerIdentifier, timeStamp) |
| : _toHoverEvent(event, position, delta, timeStamp); |
| } |
| |
| 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; |
| |
| // Update pointer state. |
| _isTracked = event is! PointerRemovedEvent; |
| _isDown = event.down; |
| |
| // 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. |
| // Note: Devices without `hover` events are expected to always have |
| // `add` and `down` events with the same position and this logic will |
| // therefor never produce `hover` events. |
| if (position != _position) { |
| final Offset delta = position - _position; |
| callback(_toMoveOrHoverEvent(event, position, delta, _pointerIdentifier, sampleTime, wasDown)); |
| _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)); |
| _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; |
| } |