Unverified Commit 02612bfe authored by David Reveman's avatar David Reveman Committed by GitHub

Pointer event resampler (#41118) (#60558)

parent 2f937703
...@@ -26,6 +26,7 @@ export 'src/gestures/multitap.dart'; ...@@ -26,6 +26,7 @@ export 'src/gestures/multitap.dart';
export 'src/gestures/pointer_router.dart'; export 'src/gestures/pointer_router.dart';
export 'src/gestures/pointer_signal_resolver.dart'; export 'src/gestures/pointer_signal_resolver.dart';
export 'src/gestures/recognizer.dart'; export 'src/gestures/recognizer.dart';
export 'src/gestures/resampler.dart';
export 'src/gestures/scale.dart'; export 'src/gestures/scale.dart';
export 'src/gestures/tap.dart'; export 'src/gestures/tap.dart';
export 'src/gestures/team.dart'; export 'src/gestures/team.dart';
......
...@@ -8,6 +8,7 @@ import 'dart:collection'; ...@@ -8,6 +8,7 @@ import 'dart:collection';
import 'dart:ui' as ui show PointerDataPacket; import 'dart:ui' as ui show PointerDataPacket;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'arena.dart'; import 'arena.dart';
import 'converter.dart'; import 'converter.dart';
...@@ -16,6 +17,131 @@ import 'events.dart'; ...@@ -16,6 +17,131 @@ import 'events.dart';
import 'hit_test.dart'; import 'hit_test.dart';
import 'pointer_router.dart'; import 'pointer_router.dart';
import 'pointer_signal_resolver.dart'; import 'pointer_signal_resolver.dart';
import 'resampler.dart';
typedef HandleSampleTimeChangedCallback = void Function();
// Class that handles resampling of touch events for multiple pointer
// devices.
//
// SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine
// sample time.
class _Resampler {
_Resampler(this._handlePointerEvent, this._handleSampleTimeChanged);
// 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;
// Current frame time for resampling.
Duration _frameTime = Duration.zero;
// 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;
// Enqueue `events` for resampling or dispatch them directly if
// not a touch event.
void addOrDispatchAll(Queue<PointerEvent> events) {
final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null);
while (events.isNotEmpty) {
final PointerEvent event = events.removeFirst();
// 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.
//
// `samplingOffset` is relative to the current frame time, which
// can be in the past when we're not actively resampling.
// `currentSystemFrameTimeStamp` is used to determine the current
// frame time.
void sample(Duration samplingOffset) {
final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null);
// 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;
// Iterate over active resamplers and sample pointer events for
// current sample time.
for (final PointerEventResampler resampler in _resamplers.values) {
resampler.sample(sampleTime, _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;
// Schedule a frame callback if another call to `sample` is needed.
if (!_frameCallbackScheduled && _resamplers.isNotEmpty) {
_frameCallbackScheduled = true;
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;
assert(() {
if (debugPrintResamplingMargin) {
final Duration resamplingMargin = _lastEventTime - _lastSampleTime;
debugPrint('$resamplingMargin');
}
return true;
}());
_handleSampleTimeChanged();
});
}
}
// Stop all resampling and dispatched any queued events.
void stop() {
for (final PointerEventResampler resampler in _resamplers.values) {
resampler.stop(_handlePointerEvent);
}
_resamplers.clear();
}
}
// 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);
/// A binding for the gesture subsystem. /// A binding for the gesture subsystem.
/// ///
...@@ -99,6 +225,17 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -99,6 +225,17 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _flushPointerEventQueue() { void _flushPointerEventQueue() {
assert(!locked); assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatchAll(_pendingPointerEvents);
_resampler.sample(samplingOffset);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
while (_pendingPointerEvents.isNotEmpty) while (_pendingPointerEvents.isNotEmpty)
_handlePointerEvent(_pendingPointerEvents.removeFirst()); _handlePointerEvent(_pendingPointerEvents.removeFirst());
} }
...@@ -226,6 +363,38 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -226,6 +363,38 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
pointerSignalResolver.resolve(event); pointerSignalResolver.resolve(event);
} }
} }
void _handleSampleTimeChanged() {
if (!locked) {
_flushPointerEventQueue();
}
}
// Resampler used to filter incoming pointer events when resampling
// is enabled.
late final _Resampler _resampler = _Resampler(
_handlePointerEvent,
_handleSampleTimeChanged,
);
/// 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 /// Variant of [FlutterErrorDetails] with extra fields for the gesture
......
...@@ -50,6 +50,20 @@ bool debugPrintGestureArenaDiagnostics = false; ...@@ -50,6 +50,20 @@ bool debugPrintGestureArenaDiagnostics = false;
/// arenas. /// arenas.
bool debugPrintRecognizerCallbacksTrace = false; bool debugPrintRecognizerCallbacksTrace = false;
/// Whether to print the resampling margin to the console.
///
/// When this is set, in debug mode, any time resampling is triggered by the
/// [GestureBinding] the resampling margin is dumped to the console. The
/// resampling margin is the delta between the time of the last received
/// touch event and the current sample time. Positive value indicates that
/// resampling is effective and the resampling offset can potentially be
/// reduced for improved latency. Negative value indicates that resampling
/// is failing and resampling offset needs to be increased for smooth
/// touch event processing.
///
/// This has no effect in release builds.
bool debugPrintResamplingMargin = false;
/// Returns true if none of the gestures library debug variables have been changed. /// Returns true if none of the gestures library debug variables have been changed.
/// ///
/// This function is used by the test framework to ensure that debug variables /// This function is used by the test framework to ensure that debug variables
...@@ -61,7 +75,8 @@ bool debugAssertAllGesturesVarsUnset(String reason) { ...@@ -61,7 +75,8 @@ bool debugAssertAllGesturesVarsUnset(String reason) {
assert(() { assert(() {
if (debugPrintHitTestResults || if (debugPrintHitTestResults ||
debugPrintGestureArenaDiagnostics || debugPrintGestureArenaDiagnostics ||
debugPrintRecognizerCallbacksTrace) debugPrintRecognizerCallbacksTrace ||
debugPrintResamplingMargin)
throw FlutterError(reason); throw FlutterError(reason);
return true; return true;
}()); }());
......
...@@ -517,6 +517,36 @@ abstract class PointerEvent with Diagnosticable { ...@@ -517,6 +517,36 @@ abstract class PointerEvent with Diagnosticable {
/// overridden by the provided `transform`. /// overridden by the provided `transform`.
PointerEvent transformed(Matrix4? transform); PointerEvent transformed(Matrix4? transform);
/// Creates a copy of event with the specified properties replaced.
PointerEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
});
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -682,6 +712,56 @@ class PointerAddedEvent extends PointerEvent { ...@@ -682,6 +712,56 @@ class PointerAddedEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerAddedEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerAddedEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
transform: transform ?? this.transform,
original: original as PointerAddedEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The device is no longer tracking the pointer. /// The device is no longer tracking the pointer.
...@@ -747,6 +827,53 @@ class PointerRemovedEvent extends PointerEvent { ...@@ -747,6 +827,53 @@ class PointerRemovedEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerRemovedEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerRemovedEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distanceMax: distanceMax ?? this.distanceMax,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
transform: transform ?? this.transform,
original: original as PointerRemovedEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has moved with respect to the device while the pointer is not /// The pointer has moved with respect to the device while the pointer is not
...@@ -857,6 +984,63 @@ class PointerHoverEvent extends PointerEvent { ...@@ -857,6 +984,63 @@ class PointerHoverEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerHoverEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerHoverEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
delta: delta ?? this.delta,
localDelta: localDelta ?? this.localDelta,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
synthesized: synthesized ?? this.synthesized,
transform: transform ?? this.transform,
original: original as PointerHoverEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has moved with respect to the device while the pointer is or is /// The pointer has moved with respect to the device while the pointer is or is
...@@ -1008,6 +1192,63 @@ class PointerEnterEvent extends PointerEvent { ...@@ -1008,6 +1192,63 @@ class PointerEnterEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerEnterEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerEnterEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
delta: delta ?? this.delta,
localDelta: localDelta ?? this.localDelta,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
synthesized: synthesized ?? this.synthesized,
transform: transform ?? this.transform,
original: original as PointerEnterEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has moved with respect to the device while the pointer is or is /// The pointer has moved with respect to the device while the pointer is or is
...@@ -1159,6 +1400,63 @@ class PointerExitEvent extends PointerEvent { ...@@ -1159,6 +1400,63 @@ class PointerExitEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerExitEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerExitEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
delta: delta ?? this.delta,
localDelta: localDelta ?? this.localDelta,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
synthesized: synthesized ?? this.synthesized,
transform: transform ?? this.transform,
original: original as PointerExitEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has made contact with the device. /// The pointer has made contact with the device.
...@@ -1251,6 +1549,61 @@ class PointerDownEvent extends PointerEvent { ...@@ -1251,6 +1549,61 @@ class PointerDownEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerDownEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerDownEvent(
timeStamp: timeStamp ?? this.timeStamp,
pointer: pointer ?? this.pointer,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressure: pressure ?? this.pressure,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
transform: transform ?? this.transform,
original: original as PointerDownEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has moved with respect to the device while the pointer is in /// The pointer has moved with respect to the device while the pointer is in
...@@ -1365,6 +1718,64 @@ class PointerMoveEvent extends PointerEvent { ...@@ -1365,6 +1718,64 @@ class PointerMoveEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerMoveEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerMoveEvent(
timeStamp: timeStamp ?? this.timeStamp,
pointer: pointer ?? this.pointer,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
delta: delta ?? this.delta,
localDelta: localDelta ?? this.localDelta,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressure: pressure ?? this.pressure,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
synthesized: synthesized ?? this.synthesized,
transform: transform ?? this.transform,
original: original as PointerMoveEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// The pointer has stopped making contact with the device. /// The pointer has stopped making contact with the device.
...@@ -1461,6 +1872,62 @@ class PointerUpEvent extends PointerEvent { ...@@ -1461,6 +1872,62 @@ class PointerUpEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerUpEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerUpEvent(
timeStamp: timeStamp ?? this.timeStamp,
pointer: pointer ?? this.pointer,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressure: pressure ?? this.pressure,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
transform: transform ?? this.transform,
original: original as PointerUpEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
/// An event that corresponds to a discrete pointer signal. /// An event that corresponds to a discrete pointer signal.
...@@ -1559,6 +2026,48 @@ class PointerScrollEvent extends PointerSignalEvent { ...@@ -1559,6 +2026,48 @@ class PointerScrollEvent extends PointerSignalEvent {
); );
} }
@override
PointerScrollEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerScrollEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
scrollDelta: scrollDelta,
transform: transform ?? this.transform,
original: original as PointerScrollEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -1656,4 +2165,59 @@ class PointerCancelEvent extends PointerEvent { ...@@ -1656,4 +2165,59 @@ class PointerCancelEvent extends PointerEvent {
embedderId: embedderId, embedderId: embedderId,
); );
} }
@override
PointerCancelEvent copyWith({
Duration? timeStamp,
int? pointer,
PointerDeviceKind? kind,
int? device,
Offset? position,
Offset? localPosition,
Offset? delta,
Offset? localDelta,
int? buttons,
bool? obscured,
double? pressure,
double? pressureMin,
double? pressureMax,
double? distance,
double? distanceMax,
double? size,
double? radiusMajor,
double? radiusMinor,
double? radiusMin,
double? radiusMax,
double? orientation,
double? tilt,
bool? synthesized,
Matrix4? transform,
PointerEvent? original,
int? embedderId,
}) {
return PointerCancelEvent(
timeStamp: timeStamp ?? this.timeStamp,
pointer: pointer ?? this.pointer,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
localPosition: localPosition ?? this.localPosition,
buttons: buttons ?? this.buttons,
obscured: obscured ?? this.obscured,
pressureMin: pressureMin ?? this.pressureMin,
pressureMax: pressureMax ?? this.pressureMax,
distance: distance ?? this.distance,
distanceMax: distanceMax ?? this.distanceMax,
size: size ?? this.size,
radiusMajor: radiusMajor ?? this.radiusMajor,
radiusMinor: radiusMinor ?? this.radiusMinor,
radiusMin: radiusMin ?? this.radiusMin,
radiusMax: radiusMax ?? this.radiusMax,
orientation: orientation ?? this.orientation,
tilt: tilt ?? this.tilt,
transform: transform ?? this.transform,
original: original as PointerCancelEvent? ?? this,
embedderId: embedderId ?? this.embedderId,
);
}
} }
// 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';
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,
);
}
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,
HandleEventCallback callback,
) {
while (_queuedEvents.isNotEmpty) {
final PointerEvent event = _queuedEvents.first;
// Potentially stop dispatching events if more recent than `sampleTime`.
if (event.timeStamp > sampleTime) {
// Stop if event is not up or removed. Otherwise, continue to
// allow early processing of up and remove events as this improves
// resampling of these events, which is important for fling
// animations.
if (event is! PointerUpEvent && event is! PointerRemovedEvent) {
break;
}
// When this line is reached, the following two invariants hold:
// (1) `event.timeStamp > sampleTime`
// (2) `_next` has the smallest time stamp that's no less than
// `sampleTime`
//
// Therefore, event must satisfy `event.timeStamp >= _next.timeStamp`.
//
// Those events with the minimum `event.timeStamp == _next.timeStamp`
// time stamp are processed early for smoother fling. For events with
// `event.timeStamp > _next.timeStamp`, the following lines break the
// while loop to stop the early processing.
//
// Specifically, when `sampleTime < _next.timeStamp`, there must be
// at least one event with `_next.timeStamp == event.timeStamp`
// and that event is `_next` itself, and it will be processed early.
//
// When `sampleTime == _next.timeStamp`, all events with
// `event.timeStamp > sampleTime` must also have
// `event.timeStamp > _next.timeStamp` so no events will be processed
// early.
//
// When the input frequency is no greater than the sampling
// frequency, this early processing should guarantee that `up` and
// `remove` events are always re-sampled.
final Duration nextTimeStamp = _next?.timeStamp ?? Duration.zero;
assert(event.timeStamp >= nextTimeStamp);
if (event.timeStamp > nextTimeStamp) {
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) {
callback(event.copyWith(
position: position,
delta: position - _position,
pointer: pointerIdentifier,
timeStamp: sampleTime,
));
_position = position;
}
_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;
final PointerEvent event = _isDown
? _toMoveEvent(next, position, delta, _pointerIdentifier, sampleTime)
: _toHoverEvent(next, position, delta, sampleTime);
callback(event);
_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.
void sample(Duration sampleTime, HandleEventCallback callback) {
_processPointerEvents(sampleTime);
// Dequeue and sample pointer events until `sampleTime`.
_dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, 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;
}
...@@ -498,6 +498,286 @@ void main() { ...@@ -498,6 +498,286 @@ void main() {
localPosition: localPosition, localPosition: localPosition,
); );
}); });
group('copyWith', () {
const PointerEvent added = PointerAddedEvent(
timeStamp: Duration(days: 1),
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distance: 11,
distanceMax: 110,
radiusMin: 1.1,
radiusMax: 22,
orientation: 1.1,
tilt: 1.1,
);
const PointerEvent hover = PointerHoverEvent(
timeStamp: Duration(days: 1),
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
buttons: 7,
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distance: 11,
distanceMax: 110,
size: 11,
radiusMajor: 11,
radiusMinor: 9,
radiusMin: 1.1,
radiusMax: 22,
orientation: 1.1,
tilt: 1.1,
synthesized: true,
);
const PointerEvent down = PointerDownEvent(
timeStamp: Duration(days: 1),
pointer: 1,
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
buttons: 7,
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distanceMax: 110,
size: 11,
radiusMajor: 11,
radiusMinor: 9,
radiusMin: 1.1,
radiusMax: 22,
orientation: 1.1,
tilt: 1.1,
);
const PointerEvent move = PointerMoveEvent(
timeStamp: Duration(days: 1),
pointer: 1,
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
delta: Offset(1.0, 2.0),
buttons: 7,
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distanceMax: 110,
size: 11,
radiusMajor: 11,
radiusMinor: 9,
radiusMin: 1.1,
radiusMax: 22,
orientation: 1.1,
tilt: 1.1,
synthesized: true,
);
const PointerEvent up = PointerUpEvent(
timeStamp: Duration(days: 1),
pointer: 1,
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
buttons: 7,
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distanceMax: 110,
size: 11,
radiusMajor: 11,
radiusMinor: 9,
radiusMin: 1.1,
radiusMax: 22,
orientation: 1.1,
tilt: 1.1,
);
const PointerEvent removed = PointerRemovedEvent(
timeStamp: Duration(days: 1),
kind: PointerDeviceKind.unknown,
device: 10,
position: Offset(101.0, 202.0),
obscured: true,
pressureMax: 2.1,
pressureMin: 1.1,
distanceMax: 110,
radiusMin: 1.1,
radiusMax: 22,
);
const Offset position = Offset.zero;
const Duration timeStamp = Duration(days: 2);
test('PointerAddedEvent.copyWith()', () {
final PointerEvent event = added.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerAddedEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, empty.pointer);
expect(event.kind, added.kind);
expect(event.device, added.device);
expect(event.position, position);
expect(event.delta, empty.delta);
expect(event.buttons, added.buttons);
expect(event.down, empty.down);
expect(event.obscured, added.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, added.pressureMin);
expect(event.pressureMax, added.pressureMax);
expect(event.distance, added.distance);
expect(event.distanceMax, added.distanceMax);
expect(event.distanceMax, added.distanceMax);
expect(event.size, empty.size);
expect(event.radiusMajor, empty.radiusMajor);
expect(event.radiusMinor, empty.radiusMinor);
expect(event.radiusMin, added.radiusMin);
expect(event.radiusMax, added.radiusMax);
expect(event.orientation, added.orientation);
expect(event.tilt, added.tilt);
expect(event.synthesized, empty.synthesized);
});
test('PointerHoverEvent.copyWith()', () {
final PointerEvent event = hover.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerHoverEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, empty.pointer);
expect(event.kind, hover.kind);
expect(event.device, hover.device);
expect(event.position, position);
expect(event.delta, empty.delta);
expect(event.buttons, hover.buttons);
expect(event.down, empty.down);
expect(event.obscured, hover.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, hover.pressureMin);
expect(event.pressureMax, hover.pressureMax);
expect(event.distance, hover.distance);
expect(event.distanceMax, hover.distanceMax);
expect(event.distanceMax, hover.distanceMax);
expect(event.size, hover.size);
expect(event.radiusMajor, hover.radiusMajor);
expect(event.radiusMinor, hover.radiusMinor);
expect(event.radiusMin, hover.radiusMin);
expect(event.radiusMax, hover.radiusMax);
expect(event.orientation, hover.orientation);
expect(event.tilt, hover.tilt);
expect(event.synthesized, hover.synthesized);
});
test('PointerDownEvent.copyWith()', () {
final PointerEvent event = down.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerDownEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, down.pointer);
expect(event.kind, down.kind);
expect(event.device, down.device);
expect(event.position, position);
expect(event.delta, empty.delta);
expect(event.buttons, down.buttons);
expect(event.down, empty.down);
expect(event.obscured, down.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, down.pressureMin);
expect(event.pressureMax, down.pressureMax);
expect(event.distance, down.distance);
expect(event.distanceMax, down.distanceMax);
expect(event.distanceMax, down.distanceMax);
expect(event.size, down.size);
expect(event.radiusMajor, down.radiusMajor);
expect(event.radiusMinor, down.radiusMinor);
expect(event.radiusMin, down.radiusMin);
expect(event.radiusMax, down.radiusMax);
expect(event.orientation, down.orientation);
expect(event.tilt, down.tilt);
expect(event.synthesized, empty.synthesized);
});
test('PointerMoveEvent.copyWith()', () {
final PointerEvent event = move.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerMoveEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, move.pointer);
expect(event.kind, move.kind);
expect(event.device, move.device);
expect(event.position, position);
expect(event.delta, move.delta);
expect(event.buttons, move.buttons);
expect(event.down, move.down);
expect(event.obscured, move.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, move.pressureMin);
expect(event.pressureMax, move.pressureMax);
expect(event.distance, move.distance);
expect(event.distanceMax, move.distanceMax);
expect(event.distanceMax, move.distanceMax);
expect(event.size, move.size);
expect(event.radiusMajor, move.radiusMajor);
expect(event.radiusMinor, move.radiusMinor);
expect(event.radiusMin, move.radiusMin);
expect(event.radiusMax, move.radiusMax);
expect(event.orientation, move.orientation);
expect(event.tilt, move.tilt);
expect(event.synthesized, move.synthesized);
});
test('PointerUpEvent.copyWith()', () {
final PointerEvent event = up.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerUpEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, up.pointer);
expect(event.kind, up.kind);
expect(event.device, up.device);
expect(event.position, position);
expect(event.delta, up.delta);
expect(event.buttons, up.buttons);
expect(event.down, empty.down);
expect(event.obscured, up.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, up.pressureMin);
expect(event.pressureMax, up.pressureMax);
expect(event.distance, up.distance);
expect(event.distanceMax, up.distanceMax);
expect(event.distanceMax, up.distanceMax);
expect(event.size, up.size);
expect(event.radiusMajor, up.radiusMajor);
expect(event.radiusMinor, up.radiusMinor);
expect(event.radiusMin, up.radiusMin);
expect(event.radiusMax, up.radiusMax);
expect(event.orientation, up.orientation);
expect(event.tilt, up.tilt);
expect(event.synthesized, empty.synthesized);
});
test('PointerRemovedEvent.copyWith()', () {
final PointerEvent event = removed.copyWith(position: position, timeStamp: timeStamp);
const PointerEvent empty = PointerRemovedEvent();
expect(event.timeStamp, timeStamp);
expect(event.pointer, empty.pointer);
expect(event.kind, removed.kind);
expect(event.device, removed.device);
expect(event.position, position);
expect(event.delta, empty.delta);
expect(event.buttons, removed.buttons);
expect(event.down, empty.down);
expect(event.obscured, removed.obscured);
expect(event.pressure, empty.pressure);
expect(event.pressureMin, removed.pressureMin);
expect(event.pressureMax, removed.pressureMax);
expect(event.distance, empty.distance);
expect(event.distanceMax, removed.distanceMax);
expect(event.distanceMax, removed.distanceMax);
expect(event.size, empty.size);
expect(event.radiusMajor, empty.radiusMajor);
expect(event.radiusMinor, empty.radiusMinor);
expect(event.radiusMin, removed.radiusMin);
expect(event.radiusMax, removed.radiusMax);
expect(event.orientation, empty.orientation);
expect(event.tilt, empty.tilt);
expect(event.synthesized, empty.synthesized);
});
});
} }
void _expectTransformedEvent({ void _expectTransformedEvent({
......
...@@ -8,13 +8,16 @@ import 'dart:ui' as ui; ...@@ -8,13 +8,16 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
typedef HandleEventCallback = void Function(PointerEvent event); typedef HandleEventCallback = void Function(PointerEvent event);
class TestGestureFlutterBinding extends BindingBase with GestureBinding { 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) {
...@@ -22,6 +25,18 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding { ...@@ -22,6 +25,18 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding {
if (callback != null) if (callback != null)
callback(event); callback(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(); TestGestureFlutterBinding _binding = TestGestureFlutterBinding();
...@@ -29,6 +44,7 @@ TestGestureFlutterBinding _binding = TestGestureFlutterBinding(); ...@@ -29,6 +44,7 @@ TestGestureFlutterBinding _binding = TestGestureFlutterBinding();
void ensureTestGestureBinding() { void ensureTestGestureBinding() {
_binding ??= TestGestureFlutterBinding(); _binding ??= TestGestureFlutterBinding();
assert(GestureBinding.instance != null); assert(GestureBinding.instance != null);
assert(SchedulerBinding.instance != null);
} }
void main() { void main() {
...@@ -299,4 +315,107 @@ void main() { ...@@ -299,4 +315,107 @@ void main() {
expect(events[4].buttons, equals(0)); expect(events[4].buttons, equals(0));
} }
}); });
test('Pointer event resampling', () {
const ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.add,
physicalX: 0.0,
timeStamp: Duration(milliseconds: 0),
),
ui.PointerData(
change: ui.PointerChange.down,
physicalX: 0.0,
timeStamp: Duration(milliseconds: 1),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 10.0,
timeStamp: Duration(milliseconds: 2),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 20.0,
timeStamp: Duration(milliseconds: 3),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 30.0,
timeStamp: Duration(milliseconds: 4),
),
ui.PointerData(
change: ui.PointerChange.up,
physicalX: 40.0,
timeStamp: Duration(milliseconds: 5),
),
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 40.0,
timeStamp: Duration(milliseconds: 6),
),
],
);
final List<PointerEvent> pointerRouterEvents = <PointerEvent>[];
GestureBinding.instance.pointerRouter.addGlobalRoute(pointerRouterEvents.add);
final List<PointerEvent> events = <PointerEvent>[];
_binding.callback = events.add;
GestureBinding.instance.resamplingEnabled = true;
GestureBinding.instance.samplingOffset = const Duration(microseconds: -5500);
ui.window.onPointerDataPacket(packet);
// No pointer events should have been dispatched yet.
expect(events.length, 0);
// Frame callback should have been requested.
FrameCallback callback = _binding.frameCallback;
_binding.frameCallback = null;
expect(callback, isNotNull);
_binding.frameTime = const Duration(milliseconds: 7);
callback(Duration.zero);
// One pointer event should have been dispatched.
expect(events.length, 1);
expect(events[0].runtimeType, equals(PointerDownEvent));
expect(events[0].timeStamp, _binding.frameTime + GestureBinding.instance.samplingOffset);
expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0));
// Second frame callback should have been requested.
callback = _binding.frameCallback;
_binding.frameCallback = null;
expect(callback, isNotNull);
_binding.frameTime = const Duration(milliseconds: 9);
callback(Duration.zero);
// Second pointer event should have been dispatched.
expect(events.length, 2);
expect(events[1].timeStamp, _binding.frameTime + GestureBinding.instance.samplingOffset);
expect(events[1].runtimeType, equals(PointerMoveEvent));
expect(events[1].position, Offset(25.0 / ui.window.devicePixelRatio, 0.0));
expect(events[1].delta, Offset(20.0 / ui.window.devicePixelRatio, 0.0));
// Third frame callback should have been requested.
callback = _binding.frameCallback;
_binding.frameCallback = null;
expect(callback, isNotNull);
_binding.frameTime = const Duration(milliseconds: 11);
callback(Duration.zero);
// Third pointer event should have been dispatched.
expect(events.length, 3);
expect(events[2].timeStamp, _binding.frameTime + GestureBinding.instance.samplingOffset);
expect(events[2].runtimeType, equals(PointerUpEvent));
expect(events[2].position, Offset(40.0 / ui.window.devicePixelRatio, 0.0));
// No frame callback should have been requested.
callback = _binding.frameCallback;
expect(callback, isNull);
});
} }
// 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.
// @dart = 2.8
import 'dart:ui';
import 'package:flutter/gestures.dart';
import '../flutter_test_alternative.dart';
void main() {
PointerEvent _createSimulatedPointerAddedEvent(
int timeStampUs,
double x,
double y,
) {
return PointerAddedEvent(
timeStamp: Duration(microseconds: timeStampUs),
position: Offset(x, y),
);
}
PointerEvent _createSimulatedPointerRemovedEvent(
int timeStampUs,
double x,
double y,
) {
return PointerRemovedEvent(
timeStamp: Duration(microseconds: timeStampUs),
position: Offset(x, y),
);
}
PointerEvent _createSimulatedPointerDownEvent(
int timeStampUs,
double x,
double y,
) {
return PointerDownEvent(
timeStamp: Duration(microseconds: timeStampUs),
position: Offset(x, y),
);
}
PointerEvent _createSimulatedPointerMoveEvent(
int timeStampUs,
double x,
double y,
double deltaX,
double deltaY,
) {
return PointerMoveEvent(
timeStamp: Duration(microseconds: timeStampUs),
position: Offset(x, y),
delta: Offset(deltaX, deltaY),
);
}
PointerEvent _createSimulatedPointerUpEvent(
int timeStampUs,
double x,
double y,
) {
return PointerUpEvent(
timeStamp: Duration(microseconds: timeStampUs),
position: Offset(x, y),
);
}
test('basic', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 50.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(2000, 10.0, 40.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(3000, 20.0, 30.0, 10.0, -10.0);
final PointerEvent event3 = _createSimulatedPointerMoveEvent(4000, 30.0, 20.0, 10.0, -10.0);
final PointerEvent event4 = _createSimulatedPointerUpEvent(5000, 40.0, 10.0);
final PointerEvent event5 = _createSimulatedPointerRemovedEvent(6000, 50.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3)
..addEvent(event4)
..addEvent(event5);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 500), result.add);
// No pointer event should have been returned yet.
expect(result.isEmpty, true);
resampler.sample(const Duration(microseconds: 1500), result.add);
// Add pointer event should have been returned.
expect(result.length, 1);
expect(result[0].timeStamp, const Duration(microseconds: 1500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 5.0);
expect(result[0].position.dy, 45.0);
resampler.sample(const Duration(microseconds: 2500), result.add);
// Down pointer event should have been returned.
expect(result.length, 2);
expect(result[1].timeStamp, const Duration(microseconds: 2500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 15.0);
expect(result[1].position.dy, 35.0);
resampler.sample(const Duration(microseconds: 3500), result.add);
// Move pointer event should have been returned.
expect(result.length, 3);
expect(result[2].timeStamp, const Duration(microseconds: 3500));
expect(result[2] is PointerMoveEvent, true);
expect(result[2].position.dx, 25.0);
expect(result[2].position.dy, 25.0);
expect(result[2].delta.dx, 10.0);
expect(result[2].delta.dy, -10.0);
resampler.sample(const Duration(microseconds: 4500), result.add);
// Up pointer event should have been returned.
expect(result.length, 4);
expect(result[3].timeStamp, const Duration(microseconds: 4500));
expect(result[3] is PointerUpEvent, true);
expect(result[3].position.dx, 35.0);
expect(result[3].position.dy, 15.0);
resampler.sample(const Duration(microseconds: 5500), result.add);
// Remove pointer event should have been returned.
expect(result.length, 5);
expect(result[4].timeStamp, const Duration(microseconds: 5500));
expect(result[4] is PointerRemovedEvent, true);
expect(result[4].position.dx, 45.0);
expect(result[4].position.dy, 5.0);
resampler.sample(const Duration(microseconds: 6500), result.add);
// No pointer event should have been returned.
expect(result.length, 5);
});
test('stream', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 50.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(2000, 10.0, 40.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(3000, 20.0, 30.0, 10.0, -10.0);
final PointerEvent event3 = _createSimulatedPointerMoveEvent(4000, 30.0, 20.0, 10.0, -10.0);
final PointerEvent event4 = _createSimulatedPointerUpEvent(5000, 40.0, 10.0);
final PointerEvent event5 = _createSimulatedPointerRemovedEvent(6000, 50.0, 0.0);
resampler.addEvent(event0);
//
// Initial sample time a 0.5 ms.
//
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 500), result.add);
// No pointer event should have been returned yet.
expect(result.isEmpty, true);
resampler.addEvent(event1);
resampler.sample(const Duration(microseconds: 500), result.add);
// No pointer event should have been returned yet.
expect(result.isEmpty, true);
//
// Advance sample time to 1.5 ms.
//
resampler.sample(const Duration(microseconds: 1500), result.add);
// Add pointer event should have been returned.
expect(result.length, 1);
expect(result[0].timeStamp, const Duration(microseconds: 1500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 5.0);
expect(result[0].position.dy, 45.0);
resampler.addEvent(event2);
resampler.sample(const Duration(microseconds: 1500), result.add);
// No more pointer events should have been returned.
expect(result.length, 1);
//
// Advance sample time to 2.5 ms.
//
resampler.sample(const Duration(microseconds: 2500), result.add);
// Down pointer event should have been returned.
expect(result.length, 2);
expect(result[1].timeStamp, const Duration(microseconds: 2500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 15.0);
expect(result[1].position.dy, 35.0);
resampler.addEvent(event3);
resampler.sample(const Duration(microseconds: 2500), result.add);
// No more pointer events should have been returned.
expect(result.length, 2);
//
// Advance sample time to 3.5 ms.
//
resampler.sample(const Duration(microseconds: 3500), result.add);
// Move pointer event should have been returned.
expect(result.length, 3);
expect(result[2].timeStamp, const Duration(microseconds: 3500));
expect(result[2] is PointerMoveEvent, true);
expect(result[2].position.dx, 25.0);
expect(result[2].position.dy, 25.0);
expect(result[2].delta.dx, 10.0);
expect(result[2].delta.dy, -10.0);
resampler.addEvent(event4);
resampler.sample(const Duration(microseconds: 3500), result.add);
// No more pointer events should have been returned.
expect(result.length, 3);
//
// Advance sample time to 4.5 ms.
//
resampler.sample(const Duration(microseconds: 4500), result.add);
// Up pointer event should have been returned.
expect(result.length, 4);
expect(result[3].timeStamp, const Duration(microseconds: 4500));
expect(result[3] is PointerUpEvent, true);
expect(result[3].position.dx, 35.0);
expect(result[3].position.dy, 15.0);
resampler.addEvent(event5);
resampler.sample(const Duration(microseconds: 4500), result.add);
// No more pointer events should have been returned.
expect(result.length, 4);
//
// Advance sample time to 5.5 ms.
//
resampler.sample(const Duration(microseconds: 5500), result.add);
// Remove pointer event should have been returned.
expect(result.length, 5);
expect(result[4].timeStamp, const Duration(microseconds: 5500));
expect(result[4] is PointerRemovedEvent, true);
expect(result[4].position.dx, 45.0);
expect(result[4].position.dy, 5.0);
//
// Advance sample time to 6.5 ms.
//
resampler.sample(const Duration(microseconds: 6500), result.add);
// No pointer events should have been returned.
expect(result.length, 5);
});
test('quick tap', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(1000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerUpEvent(1000, 0.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerRemovedEvent(1000, 0.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 1500), result.add);
// All pointer events should have been returned.
expect(result.length, 4);
expect(result[0].timeStamp, const Duration(microseconds: 1500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 0.0);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 1500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 0.0);
expect(result[1].position.dy, 0.0);
expect(result[2].timeStamp, const Duration(microseconds: 1500));
expect(result[2] is PointerUpEvent, true);
expect(result[2].position.dx, 0.0);
expect(result[2].position.dy, 0.0);
expect(result[3].timeStamp, const Duration(microseconds: 1500));
expect(result[3] is PointerRemovedEvent, true);
expect(result[3].position.dx, 0.0);
expect(result[3].position.dy, 0.0);
});
test('advance slowly', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(1000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(2000, 10.0, 0.0, 10.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerUpEvent(3000, 20.0, 0.0);
final PointerEvent event4 = _createSimulatedPointerRemovedEvent(3000, 20.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3)
..addEvent(event4);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 1500), result.add);
// Added and down pointer events should have been returned.
expect(result.length, 2);
expect(result[0].timeStamp, const Duration(microseconds: 1500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 5.0);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 1500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 5.0);
expect(result[1].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 1500), result.add);
// No pointer events should have been returned.
expect(result.length, 2);
resampler.sample(const Duration(microseconds: 1750), result.add);
// Move pointer event should have been returned.
expect(result.length, 3);
expect(result[2].timeStamp, const Duration(microseconds: 1750));
expect(result[2] is PointerMoveEvent, true);
expect(result[2].position.dx, 7.5);
expect(result[2].position.dy, 0.0);
expect(result[2].delta.dx, 2.5);
expect(result[2].delta.dy, 0.0);
resampler.sample(const Duration(microseconds: 2000), result.add);
// Another move pointer event should have been returned.
expect(result.length, 4);
expect(result[3].timeStamp, const Duration(microseconds: 2000));
expect(result[3] is PointerMoveEvent, true);
expect(result[3].position.dx, 10.0);
expect(result[3].position.dy, 0.0);
expect(result[3].delta.dx, 2.5);
expect(result[3].delta.dy, 0.0);
resampler.sample(const Duration(microseconds: 2500), result.add);
// Last two pointer events should have been returned.
expect(result.length, 6);
expect(result[4].timeStamp, const Duration(microseconds: 2500));
expect(result[4] is PointerUpEvent, true);
expect(result[4].position.dx, 15.0);
expect(result[4].position.dy, 0.0);
expect(result[5].timeStamp, const Duration(microseconds: 2500));
expect(result[5] is PointerRemovedEvent, true);
expect(result[5].position.dx, 15.0);
expect(result[5].position.dy, 0.0);
});
test('advance fast', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(1000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(2000, 5.0, 0.0, 5.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerMoveEvent(3000, 20.0, 0.0, 15.0, 0.0);
final PointerEvent event4 = _createSimulatedPointerUpEvent(4000, 30.0, 0.0);
final PointerEvent event5 = _createSimulatedPointerRemovedEvent(4000, 30.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3)
..addEvent(event4)
..addEvent(event5);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 2500), result.add);
// Add and down pointer events should have been returned.
expect(result.length, 2);
expect(result[0].timeStamp, const Duration(microseconds: 2500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 12.5);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 2500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 12.5);
expect(result[1].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 5500), result.add);
// Up and removed pointer events should have been returned.
expect(result.length, 4);
expect(result[2].timeStamp, const Duration(microseconds: 5500));
expect(result[2] is PointerUpEvent, true);
expect(result[2].position.dx, 30.0);
expect(result[2].position.dy, 0.0);
expect(result[3].timeStamp, const Duration(microseconds: 5500));
expect(result[3] is PointerRemovedEvent, true);
expect(result[3].position.dx, 30.0);
expect(result[3].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 6500), result.add);
// No pointer events should have been returned.
expect(result.length, 4);
});
test('skip', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(1000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(2000, 10.0, 0.0, 10.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerUpEvent(3000, 10.0, 0.0);
final PointerEvent event4 = _createSimulatedPointerDownEvent(4000, 20.0, 0.0);
final PointerEvent event5 = _createSimulatedPointerUpEvent(5000, 30.0, 0.0);
final PointerEvent event6 = _createSimulatedPointerRemovedEvent(5000, 30.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3)
..addEvent(event4)
..addEvent(event5)
..addEvent(event6);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 1500), result.add);
// Added and down pointer events should have been returned.
expect(result.length, 2);
expect(result[0].timeStamp, const Duration(microseconds: 1500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 5.0);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 1500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 5.0);
expect(result[1].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 4500), result.add);
// All remaining pointer events should have been returned.
expect(result.length, 6);
expect(result[2].timeStamp, const Duration(microseconds: 4500));
expect(result[2] is PointerUpEvent, true);
expect(result[2].position.dx, 25.0);
expect(result[2].position.dy, 0.0);
expect(result[3].timeStamp, const Duration(microseconds: 4500));
expect(result[3] is PointerDownEvent, true);
expect(result[3].position.dx, 25.0);
expect(result[3].position.dy, 0.0);
expect(result[4].timeStamp, const Duration(microseconds: 4500));
expect(result[4] is PointerUpEvent, true);
expect(result[4].position.dx, 25.0);
expect(result[4].position.dy, 0.0);
expect(result[5].timeStamp, const Duration(microseconds: 4500));
expect(result[5] is PointerRemovedEvent, true);
expect(result[5].position.dx, 25.0);
expect(result[5].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 5500), result.add);
// No pointer events should have been returned.
expect(result.length, 6);
});
test('skip all', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(1000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerUpEvent(4000, 30.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerRemovedEvent(4000, 30.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 500), result.add);
// No pointer events should have been returned.
expect(result.isEmpty, true);
resampler.sample(const Duration(microseconds: 5500), result.add);
// All remaining pointer events should have been returned.
expect(result.length, 4);
expect(result[0].timeStamp, const Duration(microseconds: 5500));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 30.0);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 5500));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 30.0);
expect(result[1].position.dy, 0.0);
expect(result[2].timeStamp, const Duration(microseconds: 5500));
expect(result[2] is PointerUpEvent, true);
expect(result[2].position.dx, 30.0);
expect(result[2].position.dy, 0.0);
expect(result[3].timeStamp, const Duration(microseconds: 5500));
expect(result[3] is PointerRemovedEvent, true);
expect(result[3].position.dx, 30.0);
expect(result[3].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 6500), result.add);
// No pointer events should have been returned.
expect(result.length, 4);
});
test('stop', () {
final PointerEventResampler resampler = PointerEventResampler();
final PointerEvent event0 = _createSimulatedPointerAddedEvent(1000, 0.0, 0.0);
final PointerEvent event1 = _createSimulatedPointerDownEvent(2000, 0.0, 0.0);
final PointerEvent event2 = _createSimulatedPointerMoveEvent(3000, 10.0, 0.0, 10.0, 0.0);
final PointerEvent event3 = _createSimulatedPointerUpEvent(4000, 20.0, 0.0);
final PointerEvent event4 = _createSimulatedPointerRemovedEvent(5000, 20.0, 0.0);
resampler
..addEvent(event0)
..addEvent(event1)
..addEvent(event2)
..addEvent(event3)
..addEvent(event4);
final List<PointerEvent> result = <PointerEvent>[];
resampler.sample(const Duration(microseconds: 500), result.add);
// No pointer events should have been returned.
expect(result.isEmpty, true);
resampler.stop(result.add);
// All pointer events should have been returned with orignal
// time stamps and positions.
expect(result.length, 5);
expect(result[0].timeStamp, const Duration(microseconds: 1000));
expect(result[0] is PointerAddedEvent, true);
expect(result[0].position.dx, 0.0);
expect(result[0].position.dy, 0.0);
expect(result[1].timeStamp, const Duration(microseconds: 2000));
expect(result[1] is PointerDownEvent, true);
expect(result[1].position.dx, 0.0);
expect(result[1].position.dy, 0.0);
expect(result[2].timeStamp, const Duration(microseconds: 3000));
expect(result[2] is PointerMoveEvent, true);
expect(result[2].position.dx, 10.0);
expect(result[2].position.dy, 0.0);
expect(result[2].delta.dx, 10.0);
expect(result[2].delta.dy, 0.0);
expect(result[3].timeStamp, const Duration(microseconds: 4000));
expect(result[3] is PointerUpEvent, true);
expect(result[3].position.dx, 20.0);
expect(result[3].position.dy, 0.0);
expect(result[4].timeStamp, const Duration(microseconds: 5000));
expect(result[4] is PointerRemovedEvent, true);
expect(result[4].position.dx, 20.0);
expect(result[4].position.dy, 0.0);
resampler.sample(const Duration(microseconds: 10000), result.add);
// No pointer events should have been returned.
expect(result.length, 5);
});
}
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