Unverified Commit 30a50180 authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Support trackpad gestures in framework (#89944)

* Implement trackpad gestures in framework

* Touch and Pan/Zoom pointers have separate IDs now

* Handle trackpad pointer device type

* Respect supportedDevices for pan/zoom events

* Update after rebase

* Fix check failures

* Avoid error with very short drags

* Address feedback

* Refactor drag event handler

* Address more feedback

* Add some missing punctuation
parent 38f360f9
...@@ -346,11 +346,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -346,11 +346,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handlePointerEventImmediately(PointerEvent event) { void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult; HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
assert(!_hitTests.containsKey(event.pointer)); assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult(); hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position); hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) { if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
_hitTests[event.pointer] = hitTestResult; _hitTests[event.pointer] = hitTestResult;
} }
assert(() { assert(() {
...@@ -358,9 +358,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -358,9 +358,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
debugPrint('$event: $hitTestResult'); debugPrint('$event: $hitTestResult');
return true; return true;
}()); }());
} else if (event is PointerUpEvent || event is PointerCancelEvent) { } else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
hitTestResult = _hitTests.remove(event.pointer); hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) { } else if (event.down || event is PointerPanZoomUpdateEvent) {
// Because events that occur with the pointer down (like // Because events that occur with the pointer down (like
// [PointerMoveEvent]s) should be dispatched to the same place that their // [PointerMoveEvent]s) should be dispatched to the same place that their
// initial PointerDownEvent was, we want to re-use the path we found when // initial PointerDownEvent was, we want to re-use the path we found when
...@@ -443,9 +443,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -443,9 +443,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
@override // from HitTestTarget @override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) { void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event); pointerRouter.route(event);
if (event is PointerDownEvent) { if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer); gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) { } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer); gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) { } else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event); pointerSignalResolver.resolve(event);
......
...@@ -15,14 +15,13 @@ import 'events.dart'; ...@@ -15,14 +15,13 @@ import 'events.dart';
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) { int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
switch (kind) { switch (kind) {
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
return buttons; return buttons;
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.stylus: case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
return buttons == 0 ? kPrimaryButton : buttons; return buttons == 0 ? kPrimaryButton : buttons;
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
// We have no information about the device but we know we never want // We have no information about the device but we know we never want
// buttons to be 0 when the pointer is down. // buttons to be 0 when the pointer is down.
return buttons == 0 ? kPrimaryButton : buttons; return buttons == 0 ? kPrimaryButton : buttons;
...@@ -209,9 +208,44 @@ class PointerEventConverter { ...@@ -209,9 +208,44 @@ class PointerEventConverter {
radiusMax: radiusMax, radiusMax: radiusMax,
embedderId: datum.embedderId, embedderId: datum.embedderId,
); );
default: // ignore: no_default_cases, to allow adding new pointer events to [ui.PointerChange] case ui.PointerChange.panZoomStart:
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604 return PointerPanZoomStartEvent(
throw StateError('Unreachable'); timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
embedderId: datum.embedderId,
synthesized: datum.synthesized,
);
case ui.PointerChange.panZoomUpdate:
final Offset pan =
Offset(datum.panX, datum.panY) / devicePixelRatio;
final Offset panDelta =
Offset(datum.panDeltaX, datum.panDeltaY) / devicePixelRatio;
return PointerPanZoomUpdateEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
pan: pan,
panDelta: panDelta,
scale: datum.scale,
rotation: datum.rotation,
embedderId: datum.embedderId,
synthesized: datum.synthesized,
);
case ui.PointerChange.panZoomEnd:
return PointerPanZoomEndEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
embedderId: datum.embedderId,
synthesized: datum.synthesized,
);
} }
case ui.PointerSignalKind.scroll: case ui.PointerSignalKind.scroll:
final Offset scrollDelta = final Offset scrollDelta =
......
...@@ -261,14 +261,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -261,14 +261,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
return super.isPointerAllowed(event as PointerDownEvent); return super.isPointerAllowed(event as PointerDownEvent);
} }
@override void _addPointer(PointerEvent event) {
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
_velocityTrackers[event.pointer] = velocityTrackerBuilder(event); _velocityTrackers[event.pointer] = velocityTrackerBuilder(event);
if (_state == _DragState.ready) { if (_state == _DragState.ready) {
_state = _DragState.possible; _state = _DragState.possible;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition); _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
_initialButtons = event.buttons;
_pendingDragOffset = OffsetPair.zero; _pendingDragOffset = OffsetPair.zero;
_globalDistanceMoved = 0.0; _globalDistanceMoved = 0.0;
_lastPendingEventTimestamp = event.timeStamp; _lastPendingEventTimestamp = event.timeStamp;
...@@ -279,45 +276,76 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -279,45 +276,76 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
} }
} }
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
if (_state == _DragState.ready) {
_initialButtons = event.buttons;
}
_addPointer(event);
}
@override
void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) {
super.addAllowedPointerPanZoom(event);
startTrackingPointer(event.pointer, event.transform);
if (_state == _DragState.ready) {
_initialButtons = kPrimaryButton;
}
_addPointer(event);
}
@override @override
void handleEvent(PointerEvent event) { void handleEvent(PointerEvent event) {
assert(_state != _DragState.ready); assert(_state != _DragState.ready);
if (!event.synthesized if (!event.synthesized &&
&& (event is PointerDownEvent || event is PointerMoveEvent)) { (event is PointerDownEvent ||
event is PointerMoveEvent ||
event is PointerPanZoomStartEvent ||
event is PointerPanZoomUpdateEvent)) {
final VelocityTracker tracker = _velocityTrackers[event.pointer]!; final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
assert(tracker != null); assert(tracker != null);
tracker.addPosition(event.timeStamp, event.localPosition); if (event is PointerPanZoomStartEvent) {
} tracker.addPosition(event.timeStamp, Offset.zero);
} else if (event is PointerPanZoomUpdateEvent) {
if (event is PointerMoveEvent) { tracker.addPosition(event.timeStamp, event.pan);
if (event.buttons != _initialButtons) { } else {
_giveUpPointer(event.pointer); tracker.addPosition(event.timeStamp, event.localPosition);
return;
} }
}
if (event is PointerMoveEvent && event.buttons != _initialButtons) {
_giveUpPointer(event.pointer);
return;
}
if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) {
final Offset delta = (event is PointerMoveEvent) ? event.delta : (event as PointerPanZoomUpdateEvent).panDelta;
final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
if (_state == _DragState.accepted) { if (_state == _DragState.accepted) {
_checkUpdate( _checkUpdate(
sourceTimeStamp: event.timeStamp, sourceTimeStamp: event.timeStamp,
delta: _getDeltaForDetails(event.localDelta), delta: _getDeltaForDetails(localDelta),
primaryDelta: _getPrimaryValueFromOffset(event.localDelta), primaryDelta: _getPrimaryValueFromOffset(localDelta),
globalPosition: event.position, globalPosition: position,
localPosition: event.localPosition, localPosition: localPosition,
); );
} else { } else {
_pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta); _pendingDragOffset += OffsetPair(local: localDelta, global: delta);
_lastPendingEventTimestamp = event.timeStamp; _lastPendingEventTimestamp = event.timeStamp;
_lastTransform = event.transform; _lastTransform = event.transform;
final Offset movedLocally = _getDeltaForDetails(event.localDelta); final Offset movedLocally = _getDeltaForDetails(localDelta);
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!); final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions( _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform, transform: localToGlobalTransform,
untransformedDelta: movedLocally, untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition, untransformedEndPosition: localPosition
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
} }
if (event is PointerUpEvent || event is PointerCancelEvent) { if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
_giveUpPointer(event.pointer); _giveUpPointer(event.pointer);
} }
} }
......
...@@ -96,6 +96,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -96,6 +96,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// coming from. /// coming from.
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{}; final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
/// Registers a new pointer pan/zoom that might be relevant to this gesture
/// detector.
///
/// A pointer pan/zoom is a stream of events that conveys data covering
/// pan, zoom, and rotate data from a multi-finger trackpad gesture.
///
/// The owner of this gesture recognizer calls addPointerPanZoom() with the
/// PointerPanZoomStartEvent of each pointer that should be considered for
/// this gesture.
///
/// It's the GestureRecognizer's responsibility to then add itself
/// to the global pointer router (see [PointerRouter]) to receive
/// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArenaManager]) to track
/// that pointer.
///
/// This method is called for each and all pointers being added. In
/// most cases, you want to override [addAllowedPointerPanZoom] instead.
void addPointerPanZoom(PointerPanZoomStartEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerPanZoomAllowed(event)) {
addAllowedPointerPanZoom(event);
} else {
handleNonAllowedPointerPanZoom(event);
}
}
/// Registers a new pointer pan/zoom that's been checked to be allowed by this
/// gesture recognizer.
///
/// Subclasses of [GestureRecognizer] are supposed to override this method
/// instead of [addPointerPanZoom] because [addPointerPanZoom] will be called for each
/// pointer being added while [addAllowedPointerPanZoom] is only called for pointers
/// that are allowed by this recognizer.
@protected
void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }
/// Registers a new pointer that might be relevant to this gesture /// Registers a new pointer that might be relevant to this gesture
/// detector. /// detector.
/// ///
...@@ -147,6 +184,18 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -147,6 +184,18 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
return _supportedDevices == null || _supportedDevices!.contains(event.kind); return _supportedDevices == null || _supportedDevices!.contains(event.kind);
} }
/// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
///
/// Subclasses can override this method and reject the gesture.
@protected
void handleNonAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }
/// Checks whether or not a pointer pan/zoom is allowed to be tracked by this recognizer.
@protected
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) {
return _supportedDevices == null || _supportedDevices!.contains(event.kind);
}
/// For a given pointer ID, returns the device kind associated with it. /// For a given pointer ID, returns the device kind associated with it.
/// ///
/// The pointer ID is expected to be a valid one i.e. an event was received /// The pointer ID is expected to be a valid one i.e. an event was received
...@@ -397,7 +446,7 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer { ...@@ -397,7 +446,7 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// a [PointerUpEvent] or a [PointerCancelEvent] event. /// a [PointerUpEvent] or a [PointerCancelEvent] event.
@protected @protected
void stopTrackingIfPointerNoLongerDown(PointerEvent event) { void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
if (event is PointerUpEvent || event is PointerCancelEvent) if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent)
stopTrackingPointer(event.pointer); stopTrackingPointer(event.pointer);
} }
} }
......
...@@ -2858,6 +2858,21 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event); ...@@ -2858,6 +2858,21 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event);
/// Used by [Listener] and [RenderPointerListener]. /// Used by [Listener] and [RenderPointerListener].
typedef PointerCancelEventListener = void Function(PointerCancelEvent event); typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
/// Signature for listening to [PointerPanZoomStartEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomStartEventListener = void Function(PointerPanZoomStartEvent event);
/// Signature for listening to [PointerPanZoomUpdateEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomUpdateEventListener = void Function(PointerPanZoomUpdateEvent event);
/// Signature for listening to [PointerPanZoomEndEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomEndEventListener = void Function(PointerPanZoomEndEvent event);
/// Signature for listening to [PointerSignalEvent] events. /// Signature for listening to [PointerSignalEvent] events.
/// ///
/// Used by [Listener] and [RenderPointerListener]. /// Used by [Listener] and [RenderPointerListener].
...@@ -2885,6 +2900,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2885,6 +2900,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
this.onPointerUp, this.onPointerUp,
this.onPointerHover, this.onPointerHover,
this.onPointerCancel, this.onPointerCancel,
this.onPointerPanZoomStart,
this.onPointerPanZoomUpdate,
this.onPointerPanZoomEnd,
this.onPointerSignal, this.onPointerSignal,
HitTestBehavior behavior = HitTestBehavior.deferToChild, HitTestBehavior behavior = HitTestBehavior.deferToChild,
RenderBox? child, RenderBox? child,
...@@ -2909,6 +2927,15 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2909,6 +2927,15 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// no longer directed towards this receiver. /// no longer directed towards this receiver.
PointerCancelEventListener? onPointerCancel; PointerCancelEventListener? onPointerCancel;
/// Called when a pan/zoom begins such as from a trackpad gesture.
PointerPanZoomStartEventListener? onPointerPanZoomStart;
/// Called when a pan/zoom is updated.
PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
/// Called when a pan/zoom finishes.
PointerPanZoomEndEventListener? onPointerPanZoomEnd;
/// Called when a pointer signal occurs over this object. /// Called when a pointer signal occurs over this object.
PointerSignalEventListener? onPointerSignal; PointerSignalEventListener? onPointerSignal;
...@@ -2930,6 +2957,12 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2930,6 +2957,12 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
return onPointerHover?.call(event); return onPointerHover?.call(event);
if (event is PointerCancelEvent) if (event is PointerCancelEvent)
return onPointerCancel?.call(event); return onPointerCancel?.call(event);
if (event is PointerPanZoomStartEvent)
return onPointerPanZoomStart?.call(event);
if (event is PointerPanZoomUpdateEvent)
return onPointerPanZoomUpdate?.call(event);
if (event is PointerPanZoomEndEvent)
return onPointerPanZoomEnd?.call(event);
if (event is PointerSignalEvent) if (event is PointerSignalEvent)
return onPointerSignal?.call(event); return onPointerSignal?.call(event);
} }
...@@ -2945,6 +2978,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2945,6 +2978,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
'up': onPointerUp, 'up': onPointerUp,
'hover': onPointerHover, 'hover': onPointerHover,
'cancel': onPointerCancel, 'cancel': onPointerCancel,
'panZoomStart': onPointerPanZoomStart,
'panZoomUpdate': onPointerPanZoomUpdate,
'panZoomEnd': onPointerPanZoomEnd,
'signal': onPointerSignal, 'signal': onPointerSignal,
}, },
ifEmpty: '<none>', ifEmpty: '<none>',
......
...@@ -634,6 +634,7 @@ class _AndroidMotionEventConverter { ...@@ -634,6 +634,7 @@ class _AndroidMotionEventConverter {
int toolType = AndroidPointerProperties.kToolTypeUnknown; int toolType = AndroidPointerProperties.kToolTypeUnknown;
switch (event.kind) { switch (event.kind) {
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.trackpad:
toolType = AndroidPointerProperties.kToolTypeFinger; toolType = AndroidPointerProperties.kToolTypeFinger;
break; break;
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
...@@ -646,8 +647,6 @@ class _AndroidMotionEventConverter { ...@@ -646,8 +647,6 @@ class _AndroidMotionEventConverter {
toolType = AndroidPointerProperties.kToolTypeEraser; toolType = AndroidPointerProperties.kToolTypeEraser;
break; break;
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
toolType = AndroidPointerProperties.kToolTypeUnknown; toolType = AndroidPointerProperties.kToolTypeUnknown;
break; break;
} }
......
...@@ -6090,6 +6090,9 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -6090,6 +6090,9 @@ class Listener extends SingleChildRenderObjectWidget {
this.onPointerUp, this.onPointerUp,
this.onPointerHover, this.onPointerHover,
this.onPointerCancel, this.onPointerCancel,
this.onPointerPanZoomStart,
this.onPointerPanZoomUpdate,
this.onPointerPanZoomEnd,
this.onPointerSignal, this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild, this.behavior = HitTestBehavior.deferToChild,
Widget? child, Widget? child,
...@@ -6119,6 +6122,15 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -6119,6 +6122,15 @@ class Listener extends SingleChildRenderObjectWidget {
/// no longer directed towards this receiver. /// no longer directed towards this receiver.
final PointerCancelEventListener? onPointerCancel; final PointerCancelEventListener? onPointerCancel;
/// Called when a pan/zoom begins such as from a trackpad gesture.
final PointerPanZoomStartEventListener? onPointerPanZoomStart;
/// Called when a pan/zoom is updated.
final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
/// Called when a pan/zoom finishes.
final PointerPanZoomEndEventListener? onPointerPanZoomEnd;
/// Called when a pointer signal occurs over this object. /// Called when a pointer signal occurs over this object.
/// ///
/// See also: /// See also:
...@@ -6138,6 +6150,9 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -6138,6 +6150,9 @@ class Listener extends SingleChildRenderObjectWidget {
onPointerUp: onPointerUp, onPointerUp: onPointerUp,
onPointerHover: onPointerHover, onPointerHover: onPointerHover,
onPointerCancel: onPointerCancel, onPointerCancel: onPointerCancel,
onPointerPanZoomStart: onPointerPanZoomStart,
onPointerPanZoomUpdate: onPointerPanZoomUpdate,
onPointerPanZoomEnd: onPointerPanZoomEnd,
onPointerSignal: onPointerSignal, onPointerSignal: onPointerSignal,
behavior: behavior, behavior: behavior,
); );
...@@ -6151,6 +6166,9 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -6151,6 +6166,9 @@ class Listener extends SingleChildRenderObjectWidget {
..onPointerUp = onPointerUp ..onPointerUp = onPointerUp
..onPointerHover = onPointerHover ..onPointerHover = onPointerHover
..onPointerCancel = onPointerCancel ..onPointerCancel = onPointerCancel
..onPointerPanZoomStart = onPointerPanZoomStart
..onPointerPanZoomUpdate = onPointerPanZoomUpdate
..onPointerPanZoomEnd = onPointerPanZoomEnd
..onPointerSignal = onPointerSignal ..onPointerSignal = onPointerSignal
..behavior = behavior; ..behavior = behavior;
} }
...@@ -6164,6 +6182,9 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -6164,6 +6182,9 @@ class Listener extends SingleChildRenderObjectWidget {
if (onPointerUp != null) 'up', if (onPointerUp != null) 'up',
if (onPointerHover != null) 'hover', if (onPointerHover != null) 'hover',
if (onPointerCancel != null) 'cancel', if (onPointerCancel != null) 'cancel',
if (onPointerPanZoomStart != null) 'panZoomStart',
if (onPointerPanZoomUpdate != null) 'panZoomUpdate',
if (onPointerPanZoomEnd != null) 'panZoomEnd',
if (onPointerSignal != null) 'signal', if (onPointerSignal != null) 'signal',
]; ];
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>')); properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
......
...@@ -1648,9 +1648,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1648,9 +1648,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
expectedMode = FocusHighlightMode.touch; expectedMode = FocusHighlightMode.touch;
break; break;
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
_lastInteractionWasTouch = false; _lastInteractionWasTouch = false;
expectedMode = FocusHighlightMode.traditional; expectedMode = FocusHighlightMode.traditional;
break; break;
......
...@@ -1435,6 +1435,13 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -1435,6 +1435,13 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
recognizer.addPointer(event); recognizer.addPointer(event);
} }
void _handlePointerPanZoomStart(PointerPanZoomStartEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values) {
recognizer.addPointerPanZoom(event);
}
}
HitTestBehavior get _defaultBehavior { HitTestBehavior get _defaultBehavior {
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild; return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
} }
...@@ -1449,6 +1456,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -1449,6 +1456,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget result = Listener( Widget result = Listener(
onPointerDown: _handlePointerDown, onPointerDown: _handlePointerDown,
onPointerPanZoomStart: _handlePointerPanZoomStart,
behavior: widget.behavior ?? _defaultBehavior, behavior: widget.behavior ?? _defaultBehavior,
child: widget.child, child: widget.child,
); );
......
...@@ -19,6 +19,7 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ ...@@ -19,6 +19,7 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.stylus, PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus, PointerDeviceKind.invertedStylus,
PointerDeviceKind.trackpad,
// The VoiceAccess sends pointer events with unknown type when scrolling // The VoiceAccess sends pointer events with unknown type when scrolling
// scrollables. // scrollables.
PointerDeviceKind.unknown, PointerDeviceKind.unknown,
......
...@@ -682,13 +682,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -682,13 +682,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
switch (kind) { switch (kind) {
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.trackpad:
return paddedRect.contains(position); return paddedRect.contains(position);
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.stylus: case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
return interactiveRect.contains(position); return interactiveRect.contains(position);
} }
} }
...@@ -713,6 +712,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -713,6 +712,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
switch (kind) { switch (kind) {
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.trackpad:
final Rect touchThumbRect = _thumbRect!.expandToInclude( final Rect touchThumbRect = _thumbRect!.expandToInclude(
Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2), Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
); );
...@@ -721,8 +721,6 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -721,8 +721,6 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
case PointerDeviceKind.stylus: case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
return _thumbRect!.contains(position); return _thumbRect!.contains(position);
} }
} }
...@@ -1963,6 +1961,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1963,6 +1961,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
onExit: (PointerExitEvent event) { onExit: (PointerExitEvent event) {
switch(event.kind) { switch(event.kind) {
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
if (enableGestures) if (enableGestures)
handleHoverExit(event); handleHoverExit(event);
break; break;
...@@ -1970,14 +1969,13 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1970,14 +1969,13 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
break; break;
} }
}, },
onHover: (PointerHoverEvent event) { onHover: (PointerHoverEvent event) {
switch(event.kind) { switch(event.kind) {
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
if (enableGestures) if (enableGestures)
handleHover(event); handleHover(event);
break; break;
...@@ -1985,8 +1983,6 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1985,8 +1983,6 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
break; break;
} }
}, },
......
...@@ -1525,6 +1525,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1525,6 +1525,7 @@ class TextSelectionGestureDetectorBuilder {
case TargetPlatform.macOS: case TargetPlatform.macOS:
switch (details.kind) { switch (details.kind) {
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.stylus: case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
// Precise devices should place the cursor at a precise position. // Precise devices should place the cursor at a precise position.
...@@ -1532,8 +1533,6 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1532,8 +1533,6 @@ class TextSelectionGestureDetectorBuilder {
break; break;
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge // On macOS/iOS/iPadOS a touch tap places the cursor at the edge
// of the word. // of the word.
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
......
...@@ -1484,4 +1484,219 @@ void main() { ...@@ -1484,4 +1484,219 @@ void main() {
tap2.dispose(); tap2.dispose();
}); });
testGesture('Should recognize pan gestures from platform', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer();
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
final PanGestureRecognizer competingPan = PanGestureRecognizer();
addTearDown(pan.dispose);
addTearDown(competingPan.dispose);
bool didStartPan = false;
pan.onStart = (_) {
didStartPan = true;
};
Offset? updatedScrollDelta;
pan.onUpdate = (DragUpdateDetails details) {
updatedScrollDelta = details.delta;
};
bool didEndPan = false;
pan.onEnd = (DragEndDetails details) {
didEndPan = true;
};
final TestPointer pointer = TestPointer(2);
final PointerPanZoomStartEvent start = pointer.panZoomStart(const Offset(10.0, 10.0));
pan.addPointerPanZoom(start);
competingPan.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(start);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
expect(didStartPan, isFalse); // 28 < 36
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
expect(didStartPan, isTrue); // 42 > 36
didStartPan = false;
expect(didEndPan, isFalse);
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 25.0)));
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(0.0, -5.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
tester.route(pointer.panZoomEnd());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isTrue);
didEndPan = false;
});
testGesture('Pointer pan/zooms drags should allow touches to join them', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer();
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
final PanGestureRecognizer competingPan = PanGestureRecognizer();
addTearDown(pan.dispose);
addTearDown(competingPan.dispose);
bool didStartPan = false;
pan.onStart = (_) {
didStartPan = true;
};
Offset? updatedScrollDelta;
pan.onUpdate = (DragUpdateDetails details) {
updatedScrollDelta = details.delta;
};
bool didEndPan = false;
pan.onEnd = (DragEndDetails details) {
didEndPan = true;
};
final TestPointer panZoomPointer = TestPointer(2);
final TestPointer touchPointer = TestPointer(3);
final PointerPanZoomStartEvent start = panZoomPointer.panZoomStart(const Offset(10.0, 10.0));
pan.addPointerPanZoom(start);
competingPan.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(start);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
expect(didStartPan, isFalse); // 28 < 36
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
expect(didStartPan, isTrue); // 42 > 36
didStartPan = false;
expect(didEndPan, isFalse);
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 25.0)));
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(0.0, -5.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
final PointerDownEvent touchDown = touchPointer.down(const Offset(20.0, 20.0));
pan.addPointer(touchDown);
competingPan.addPointer(touchDown);
tester.closeArena(3);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(touchPointer.move(const Offset(25.0, 25.0)));
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(5.0, 5.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
tester.route(touchPointer.up());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(panZoomPointer.panZoomEnd());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isTrue);
didEndPan = false;
});
testGesture('Touch drags should allow pointer pan/zooms to join them', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer();
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
final PanGestureRecognizer competingPan = PanGestureRecognizer();
addTearDown(pan.dispose);
addTearDown(competingPan.dispose);
bool didStartPan = false;
pan.onStart = (_) {
didStartPan = true;
};
Offset? updatedScrollDelta;
pan.onUpdate = (DragUpdateDetails details) {
updatedScrollDelta = details.delta;
};
bool didEndPan = false;
pan.onEnd = (DragEndDetails details) {
didEndPan = true;
};
final TestPointer panZoomPointer = TestPointer(2);
final TestPointer touchPointer = TestPointer(3);
final PointerDownEvent touchDown = touchPointer.down(const Offset(20.0, 20.0));
pan.addPointer(touchDown);
competingPan.addPointer(touchDown);
tester.closeArena(3);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(touchPointer.move(const Offset(60.0, 60.0)));
expect(didStartPan, isTrue);
didStartPan = false;
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(touchPointer.move(const Offset(70.0, 70.0)));
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(10.0, 10.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
final PointerPanZoomStartEvent start = panZoomPointer.panZoomStart(const Offset(10.0, 10.0));
pan.addPointerPanZoom(start);
competingPan.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(start);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(20.0, 20.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(10.0, 10.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
tester.route(panZoomPointer.panZoomEnd());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
tester.route(touchPointer.up());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isTrue);
didEndPan = false;
});
} }
...@@ -505,6 +505,39 @@ void main() { ...@@ -505,6 +505,39 @@ void main() {
localPosition: localPosition, localPosition: localPosition,
); );
const PointerPanZoomStartEvent panZoomStart = PointerPanZoomStartEvent(
timeStamp: Duration(seconds: 2),
device: 1,
position: Offset(20, 30),
);
_expectTransformedEvent(
original: panZoomStart,
transform: transform,
localPosition: localPosition,
);
const PointerPanZoomUpdateEvent panZoomUpdate = PointerPanZoomUpdateEvent(
timeStamp: Duration(seconds: 2),
device: 1,
position: Offset(20, 30),
);
_expectTransformedEvent(
original: panZoomUpdate,
transform: transform,
localPosition: localPosition,
);
const PointerPanZoomEndEvent panZoomEnd = PointerPanZoomEndEvent(
timeStamp: Duration(seconds: 2),
device: 1,
position: Offset(20, 30),
);
_expectTransformedEvent(
original: panZoomEnd,
transform: transform,
localPosition: localPosition,
);
const PointerUpEvent up = PointerUpEvent( const PointerUpEvent up = PointerUpEvent(
timeStamp: Duration(seconds: 2), timeStamp: Duration(seconds: 2),
pointer: 45, pointer: 45,
......
...@@ -335,4 +335,23 @@ void main() { ...@@ -335,4 +335,23 @@ void main() {
expect(events[4].buttons, equals(0)); expect(events[4].buttons, equals(0));
} }
}); });
test('Pointer pan/zoom events', () {
const ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(change: ui.PointerChange.panZoomStart),
ui.PointerData(change: ui.PointerChange.panZoomUpdate),
ui.PointerData(change: ui.PointerChange.panZoomEnd),
],
);
final List<PointerEvent> events = <PointerEvent>[];
binding.callback = events.add;
ui.window.onPointerDataPacket?.call(packet);
expect(events.length, 3);
expect(events[0], isA<PointerPanZoomStartEvent>());
expect(events[1], isA<PointerPanZoomUpdateEvent>());
expect(events[2], isA<PointerPanZoomEndEvent>());
});
} }
...@@ -272,7 +272,7 @@ void main() { ...@@ -272,7 +272,7 @@ void main() {
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │ behavior: opaque\n' ' │ behavior: opaque\n'
' │ listeners: down\n' ' │ listeners: down, panZoomStart\n'
' │\n' ' │\n'
' └─child: RenderSemanticsAnnotations#00000\n' ' └─child: RenderSemanticsAnnotations#00000\n'
' │ needs compositing\n' ' │ needs compositing\n'
...@@ -432,7 +432,7 @@ void main() { ...@@ -432,7 +432,7 @@ void main() {
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │ behavior: opaque\n' ' │ behavior: opaque\n'
' │ listeners: down\n' ' │ listeners: down, panZoomStart\n'
' │\n' ' │\n'
' └─child: RenderSemanticsAnnotations#00000\n' ' └─child: RenderSemanticsAnnotations#00000\n'
' │ needs compositing\n' ' │ needs compositing\n'
......
...@@ -134,6 +134,7 @@ void main() { ...@@ -134,6 +134,7 @@ void main() {
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.stylus, PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus, PointerDeviceKind.invertedStylus,
PointerDeviceKind.trackpad,
PointerDeviceKind.unknown, PointerDeviceKind.unknown,
}); });
......
...@@ -33,9 +33,8 @@ class TestPointer { ...@@ -33,9 +33,8 @@ class TestPointer {
case PointerDeviceKind.stylus: case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus: case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
_device = device ?? 0; _device = device ?? 0;
break; break;
} }
...@@ -70,12 +69,27 @@ class TestPointer { ...@@ -70,12 +69,27 @@ class TestPointer {
bool get isDown => _isDown; bool get isDown => _isDown;
bool _isDown = false; bool _isDown = false;
/// Whether the pointer simulated by this object currently has
/// an active pan/zoom gesture.
///
/// A pan/zoom gesture begins when [panZoomStart] is called, and
/// ends when [panZoomEnd] is called.
bool get isPanZoomActive => _isPanZoomActive;
bool _isPanZoomActive = false;
/// The position of the last event sent by this object. /// The position of the last event sent by this object.
/// ///
/// If no event has ever been sent by this object, returns null. /// If no event has ever been sent by this object, returns null.
Offset? get location => _location; Offset? get location => _location;
Offset? _location; Offset? _location;
/// The pan offset of the last pointer pan/zoom event sent by this object.
///
/// If no pan/zoom event has ever been sent by this object, returns null.
Offset? get pan => _pan;
Offset? _pan;
/// If a custom event is created outside of this class, this function is used /// If a custom event is created outside of this class, this function is used
/// to set the [isDown]. /// to set the [isDown].
bool setDownInfo( bool setDownInfo(
...@@ -115,6 +129,7 @@ class TestPointer { ...@@ -115,6 +129,7 @@ class TestPointer {
int? buttons, int? buttons,
}) { }) {
assert(!isDown); assert(!isDown);
assert(!isPanZoomActive);
_isDown = true; _isDown = true;
_location = newLocation; _location = newLocation;
if (buttons != null) if (buttons != null)
...@@ -149,6 +164,7 @@ class TestPointer { ...@@ -149,6 +164,7 @@ class TestPointer {
'Move events can only be generated when the pointer is down. To ' 'Move events can only be generated when the pointer is down. To '
'create a movement event simulating a pointer move when the pointer is ' 'create a movement event simulating a pointer move when the pointer is '
'up, use hover() instead.'); 'up, use hover() instead.');
assert(!isPanZoomActive);
final Offset delta = newLocation - location!; final Offset delta = newLocation - location!;
_location = newLocation; _location = newLocation;
if (buttons != null) if (buttons != null)
...@@ -171,6 +187,7 @@ class TestPointer { ...@@ -171,6 +187,7 @@ class TestPointer {
/// ///
/// The object is no longer usable after this method has been called. /// The object is no longer usable after this method has been called.
PointerUpEvent up({ Duration timeStamp = Duration.zero }) { PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
assert(!isPanZoomActive);
assert(isDown); assert(isDown);
_isDown = false; _isDown = false;
return PointerUpEvent( return PointerUpEvent(
...@@ -283,6 +300,79 @@ class TestPointer { ...@@ -283,6 +300,79 @@ class TestPointer {
scrollDelta: scrollDelta, scrollDelta: scrollDelta,
); );
} }
/// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
/// or finger-drag scroll) with the given delta.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerPanZoomStartEvent panZoomStart(
Offset location, {
Duration timeStamp = Duration.zero
}) {
assert(!isPanZoomActive);
_location = location;
_pan = Offset.zero;
_isPanZoomActive = true;
return PointerPanZoomStartEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location,
);
}
/// Create a [PointerPanZoomUpdateEvent] to update the active pan/zoom sequence
/// on this pointer with updated pan, scale, and/or rotation values.
///
/// [rotation] is in units of radians.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerPanZoomUpdateEvent panZoomUpdate(
Offset location, {
Offset pan = Offset.zero,
double scale = 1,
double rotation = 0,
Duration timeStamp = Duration.zero,
}) {
assert(isPanZoomActive);
_location = location;
final Offset panDelta = pan - _pan!;
_pan = pan;
return PointerPanZoomUpdateEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location,
pan: pan,
panDelta: panDelta,
scale: scale,
rotation: rotation,
);
}
/// Create a [PointerPanZoomEndEvent] to end the active pan/zoom sequence
/// on this pointer.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerPanZoomEndEvent panZoomEnd({
Duration timeStamp = Duration.zero
}) {
assert(isPanZoomActive);
_isPanZoomActive = false;
_pan = null;
return PointerPanZoomEndEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location!,
);
}
} }
/// Signature for a callback that can dispatch events and returns a future that /// Signature for a callback that can dispatch events and returns a future that
......
...@@ -45,5 +45,7 @@ ...@@ -45,5 +45,7 @@
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>
...@@ -45,5 +45,7 @@ ...@@ -45,5 +45,7 @@
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>
...@@ -1383,6 +1383,8 @@ void main() { ...@@ -1383,6 +1383,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}); });
...@@ -1400,6 +1402,8 @@ void main() { ...@@ -1400,6 +1402,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}); });
...@@ -1417,6 +1421,8 @@ void main() { ...@@ -1417,6 +1421,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -1443,6 +1449,8 @@ void main() { ...@@ -1443,6 +1449,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -1469,6 +1477,8 @@ void main() { ...@@ -1469,6 +1477,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}); });
...@@ -1486,6 +1496,8 @@ void main() { ...@@ -1486,6 +1496,8 @@ void main() {
expect(plistFile, exists); expect(plistFile, exists);
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone'); final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
expect(disabled, isTrue); expect(disabled, isTrue);
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
expect(indirectInput, isTrue);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName'); final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project'); expect(displayName, 'My Project');
}); });
......
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