Unverified Commit 727e7e82 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Add buttons to gestures (#30339)

* Callbacks of tap, long press, drag, and double tap GR respond to only primary events.
* Add "secondary" callbacks to tap.
* Recognizers only compete on events if there are any related callbacks.
* Add "kSecondaryButton" constant.
parent c1c15dd7
...@@ -8,22 +8,35 @@ import 'package:flutter/foundation.dart'; ...@@ -8,22 +8,35 @@ import 'package:flutter/foundation.dart';
export 'dart:ui' show Offset, PointerDeviceKind; export 'dart:ui' show Offset, PointerDeviceKind;
/// The bit of [PointerEvent.buttons] that corresponds to the "primary /// The bit of [PointerEvent.buttons] that corresponds to a cross-device
/// action" on any device. /// behavior of "primary operation".
/// ///
/// More specifially, /// More specifially, it includes:
/// ///
/// * For touch screen, it's when the pointer contacts the screen. /// * [kTouchContact]: The pointer contacts the touch screen.
/// * For stylus and inverted stylus, it's when the pen contacts the screen. /// * [kStylusContact]: The stylus contacts the screen.
/// * For mouse, it's when the primary button is pressed. /// * [kPrimaryMouseButton]: The primary mouse button.
/// ///
/// See also: /// See also:
/// ///
/// * [kTouchContact]: an alias of this constant when used by touch screen. /// * [kSecondaryButton], which describes a cross-device behavior of
/// * [kStylusContact]: an alias of this constant when used by stylus. /// "secondary operation".
/// * [kPrimaryMouseButton]: an alias of this constant when used by mouse.
const int kPrimaryButton = 0x01; const int kPrimaryButton = 0x01;
/// The bit of [PointerEvent.buttons] that corresponds to a cross-device
/// behavior of "secondary operation".
///
/// It is equivalent to:
///
/// * [kPrimaryStylusButton]: The stylus contacts the screen.
/// * [kSecondaryMouseButton]: The primary mouse button.
///
/// See also:
///
/// * [kPrimaryButton], which describes a cross-device behavior of
/// "primary operation".
const int kSecondaryButton = 0x02;
/// The bit of [PointerEvent.buttons] that corresponds to the primary mouse button. /// The bit of [PointerEvent.buttons] that corresponds to the primary mouse button.
/// ///
/// The primary mouse button is typically the left button on the top of the /// The primary mouse button is typically the left button on the top of the
...@@ -31,28 +44,40 @@ const int kPrimaryButton = 0x01; ...@@ -31,28 +44,40 @@ const int kPrimaryButton = 0x01;
/// ///
/// See also: /// See also:
/// ///
/// * [kTouchContact]: an alias of this constant when used by touch screen. /// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
const int kPrimaryMouseButton = kPrimaryButton; const int kPrimaryMouseButton = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the secondary mouse button. /// The bit of [PointerEvent.buttons] that corresponds to the secondary mouse button.
/// ///
/// The secondary mouse button is typically the right button on the top of the /// The secondary mouse button is typically the right button on the top of the
/// mouse but can be reconfigured to be a different physical button. /// mouse but can be reconfigured to be a different physical button.
const int kSecondaryMouseButton = 0x02; ///
/// See also:
///
/// * [kSecondaryButton], which has the same value but describes its cross-device
/// concept.
const int kSecondaryMouseButton = kSecondaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to when a stylus /// The bit of [PointerEvent.buttons] that corresponds to when a stylus
/// contacting the screen. /// contacting the screen.
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton]: an alias of this constant for any device. /// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
const int kStylusContact = kPrimaryButton; const int kStylusContact = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the primary stylus button. /// The bit of [PointerEvent.buttons] that corresponds to the primary stylus button.
/// ///
/// The primary stylus button is typically the top of the stylus and near the /// The primary stylus button is typically the top of the stylus and near the
/// tip but can be reconfigured to be a different physical button. /// tip but can be reconfigured to be a different physical button.
const int kPrimaryStylusButton = 0x02; ///
/// See also:
///
/// * [kSecondaryButton], which has the same value but describes its cross-device
/// concept.
const int kPrimaryStylusButton = kSecondaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the middle mouse button. /// The bit of [PointerEvent.buttons] that corresponds to the middle mouse button.
/// ///
...@@ -84,7 +109,8 @@ const int kForwardMouseButton = 0x10; ...@@ -84,7 +109,8 @@ const int kForwardMouseButton = 0x10;
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton]: an alias of this constant for any device. /// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
const int kTouchContact = kPrimaryButton; const int kTouchContact = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the nth mouse button. /// The bit of [PointerEvent.buttons] that corresponds to the nth mouse button.
...@@ -104,6 +130,47 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn ...@@ -104,6 +130,47 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn
/// for some stylus buttons. /// for some stylus buttons.
int nthStylusButton(int number) => (kPrimaryStylusButton << (number - 1)) & kMaxUnsignedSMI; int nthStylusButton(int number) => (kPrimaryStylusButton << (number - 1)) & kMaxUnsignedSMI;
/// Returns the button of `buttons` with the smallest integer.
///
/// The `buttons` parameter is a bitfield where each set bit represents a button.
/// This function returns the set bit closest to the least significant bit.
///
/// It returns zero when `buttons` is zero.
///
/// Example:
///
/// ```dart
/// assert(rightmostButton(0x1) == 0x1);
/// assert(rightmostButton(0x11) == 0x1);
/// assert(rightmostButton(0) == 0);
/// ```
///
/// See also:
///
/// * [isSingleButton], which checks if a `buttons` contains exactly one button.
int smallestButton(int buttons) => buttons & (-buttons);
/// Returns whether `buttons` contains one and only one button.
///
/// The `buttons` parameter is a bitfield where each set bit represents a button.
/// This function returns whether there is only one set bit in the given integer.
///
/// It returns false when `buttons` is zero.
///
/// Example:
///
/// ```dart
/// assert(isSingleButton(0x1) == true);
/// assert(isSingleButton(0x11) == false);
/// assert(isSingleButton(0) == false);
/// ```
///
/// See also:
///
/// * [smallestButton], which returns the button in a `buttons` bitfield with
/// the smallest integer button.
bool isSingleButton(int buttons) => buttons != 0 && (smallestButton(buttons) == buttons);
/// Base class for touch, stylus, or mouse events. /// Base class for touch, stylus, or mouse events.
/// ///
/// Pointer events operate in the coordinate space of the screen, scaled to /// Pointer events operate in the coordinate space of the screen, scaled to
......
...@@ -109,6 +109,10 @@ class LongPressEndDetails { ...@@ -109,6 +109,10 @@ class LongPressEndDetails {
/// until it's recognized. Once the gesture is accepted, the finger can be /// until it's recognized. Once the gesture is accepted, the finger can be
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the /// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
/// [postAcceptSlopTolerance] constructor argument is specified. /// [postAcceptSlopTolerance] constructor argument is specified.
///
/// [LongPressGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a long-press gesture recognizer. /// Creates a long-press gesture recognizer.
/// ///
...@@ -133,89 +137,159 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -133,89 +137,159 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
); );
bool _longPressAccepted = false; bool _longPressAccepted = false;
Offset _longPressOrigin; Offset _longPressOrigin;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
/// Called when a long press gesture has been recognized. /// Called when a long press gesture by a primary button has been recognized.
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has data for the /// * [onLongPressStart], which has the same timing but has data for the
/// press location. /// press location.
GestureLongPressCallback onLongPress; GestureLongPressCallback onLongPress;
/// Callback for long press start with gesture location. /// Called when a long press gesture by a primary button has been recognized.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPress], which has the same timing but without the location data. /// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPress], which has the same timing but without details.
/// * [LongPressStartDetails], which is passed as an argument to this callback.
GestureLongPressStartCallback onLongPressStart; GestureLongPressStartCallback onLongPressStart;
/// Callback for moving the gesture after the lang press is recognized. /// Called when moving after the long press by a primary button is recognized.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressMoveUpdateDetails], which is passed as an argument to this
/// callback.
GestureLongPressMoveUpdateCallback onLongPressMoveUpdate; GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
/// Called when the pointer stops contacting the screen after the long-press. /// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has data for the up /// * [onLongPressEnd], which has the same timing but has data for the up
/// gesture location. /// gesture location.
GestureLongPressUpCallback onLongPressUp; GestureLongPressUpCallback onLongPressUp;
/// Callback for long press end with gesture location. /// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPressUp], which has the same timing but without the location data. /// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressUp], which has the same timing, but without details.
/// * [LongPressEndDetails], which is passed as an argument to this
/// callback.
GestureLongPressEndCallback onLongPressEnd; GestureLongPressEndCallback onLongPressEnd;
@override
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {
case kPrimaryButton:
if (onLongPressStart == null &&
onLongPress == null &&
onLongPressMoveUpdate == null &&
onLongPressEnd == null &&
onLongPressUp == null)
return false;
break;
default:
return false;
}
return super.isPointerAllowed(event);
}
@override @override
void didExceedDeadline() { void didExceedDeadline() {
// Exceeding the deadline puts the gesture in the accepted state.
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
_longPressAccepted = true; _longPressAccepted = true;
super.acceptGesture(primaryPointer); super.acceptGesture(primaryPointer);
if (onLongPress != null) { _checkLongPressStart();
invokeCallback<void>('onLongPress', onLongPress);
}
if (onLongPressStart != null) {
invokeCallback<void>('onLongPressStart', () {
onLongPressStart(LongPressStartDetails(
globalPosition: _longPressOrigin,
));
});
}
} }
@override @override
void handlePrimaryPointer(PointerEvent event) { void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) { if (event is PointerUpEvent) {
if (_longPressAccepted == true) { if (_longPressAccepted == true) {
if (onLongPressUp != null) { _checkLongPressEnd(event);
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
if (onLongPressEnd != null) {
invokeCallback<void>('onLongPressEnd', () {
onLongPressEnd(LongPressEndDetails(
globalPosition: event.position,
));
});
}
_longPressAccepted = false;
} else { } else {
// Pointer is lifted before timeout.
resolve(GestureDisposition.rejected); resolve(GestureDisposition.rejected);
} }
} else if (event is PointerDownEvent || event is PointerCancelEvent) { _reset();
} else if (event is PointerCancelEvent) {
_reset();
} else if (event is PointerDownEvent) {
// The first touch. // The first touch.
_longPressAccepted = false;
_longPressOrigin = event.position; _longPressOrigin = event.position;
} else if (event is PointerMoveEvent && _longPressAccepted && onLongPressMoveUpdate != null) { _initialButtons = event.buttons;
invokeCallback<void>('onLongPressMoveUpdate', () { } else if (event is PointerMoveEvent) {
onLongPressMoveUpdate(LongPressMoveUpdateDetails( if (event.buttons != _initialButtons) {
globalPosition: event.position, resolve(GestureDisposition.rejected);
offsetFromOrigin: event.position - _longPressOrigin, stopTrackingPointer(primaryPointer);
)); } else if (_longPressAccepted) {
}); _checkLongPressMoveUpdate(event);
}
}
}
void _checkLongPressStart() {
assert(_initialButtons == kPrimaryButton);
final LongPressStartDetails details = LongPressStartDetails(
globalPosition: _longPressOrigin,
);
if (onLongPressStart != null)
invokeCallback<void>('onLongPressStart',
() => onLongPressStart(details));
if (onLongPress != null)
invokeCallback<void>('onLongPress', onLongPress);
}
void _checkLongPressMoveUpdate(PointerEvent event) {
assert(_initialButtons == kPrimaryButton);
final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
globalPosition: event.position,
offsetFromOrigin: event.position - _longPressOrigin,
);
if (onLongPressMoveUpdate != null)
invokeCallback<void>('onLongPressMoveUpdate',
() => onLongPressMoveUpdate(details));
}
void _checkLongPressEnd(PointerEvent event) {
assert(_initialButtons == kPrimaryButton);
final LongPressEndDetails details = LongPressEndDetails(
globalPosition: event.position,
);
if (onLongPressEnd != null)
invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
if (onLongPressUp != null)
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
void _reset() {
_longPressAccepted = false;
_longPressOrigin = null;
_initialButtons = null;
}
@override
void resolve(GestureDisposition disposition) {
if (_longPressAccepted && disposition == GestureDisposition.rejected) {
// This can happen if the gesture has been canceled. For example when
// the buttons have changed.
_reset();
} }
super.resolve(disposition);
} }
@override @override
......
...@@ -43,6 +43,10 @@ typedef GestureDragCancelCallback = void Function(); ...@@ -43,6 +43,10 @@ typedef GestureDragCancelCallback = void Function();
/// consider using one of its subclasses to recognize specific types for drag /// consider using one of its subclasses to recognize specific types for drag
/// gestures. /// gestures.
/// ///
/// [DragGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
///
/// See also: /// See also:
/// ///
/// * [HorizontalDragGestureRecognizer], for left and right drags. /// * [HorizontalDragGestureRecognizer], for left and right drags.
...@@ -84,13 +88,20 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -84,13 +88,20 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// at (510.0, 500.0). /// at (510.0, 500.0).
DragStartBehavior dragStartBehavior; DragStartBehavior dragStartBehavior;
/// A pointer has contacted the screen and might begin to move. /// A pointer has contacted the screen with a primary button and might begin
/// to move.
/// ///
/// The position of the pointer is provided in the callback's `details` /// The position of the pointer is provided in the callback's `details`
/// argument, which is a [DragDownDetails] object. /// argument, which is a [DragDownDetails] object.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragDownDetails], which is passed as an argument to this callback.
GestureDragDownCallback onDown; GestureDragDownCallback onDown;
/// A pointer has contacted the screen and has begun to move. /// A pointer has contacted the screen with a primary button and has begun to
/// move.
/// ///
/// The position of the pointer is provided in the callback's `details` /// The position of the pointer is provided in the callback's `details`
/// argument, which is a [DragStartDetails] object. /// argument, which is a [DragStartDetails] object.
...@@ -99,23 +110,43 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -99,23 +110,43 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// called on the initial touch down, if set to [DragStartBehavior.down] or /// called on the initial touch down, if set to [DragStartBehavior.down] or
/// when the drag gesture is first detected, if set to /// when the drag gesture is first detected, if set to
/// [DragStartBehavior.start]. /// [DragStartBehavior.start].
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragStartDetails], which is passed as an argument to this callback.
GestureDragStartCallback onStart; GestureDragStartCallback onStart;
/// A pointer that is in contact with the screen and moving has moved again. /// A pointer that is in contact with the screen with a primary button and
/// moving has moved again.
/// ///
/// The distance travelled by the pointer since the last update is provided in /// The distance travelled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [DragUpdateDetails] object. /// the callback's `details` argument, which is a [DragUpdateDetails] object.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragUpdateDetails], which is passed as an argument to this callback.
GestureDragUpdateCallback onUpdate; GestureDragUpdateCallback onUpdate;
/// A pointer that was previously in contact with the screen and moving is no /// A pointer that was previously in contact with the screen with a primary
/// longer in contact with the screen and was moving at a specific velocity /// button and moving is no longer in contact with the screen and was moving
/// when it stopped contacting the screen. /// at a specific velocity when it stopped contacting the screen.
/// ///
/// The velocity is provided in the callback's `details` argument, which is a /// The velocity is provided in the callback's `details` argument, which is a
/// [DragEndDetails] object. /// [DragEndDetails] object.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragEndDetails], which is passed as an argument to this callback.
GestureDragEndCallback onEnd; GestureDragEndCallback onEnd;
/// The pointer that previously triggered [onDown] did not complete. /// The pointer that previously triggered [onDown] did not complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
GestureDragCancelCallback onCancel; GestureDragCancelCallback onCancel;
/// The minimum distance an input pointer drag must have moved to /// The minimum distance an input pointer drag must have moved to
...@@ -141,6 +172,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -141,6 +172,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset _initialPosition; Offset _initialPosition;
Offset _pendingDragOffset; Offset _pendingDragOffset;
Duration _lastPendingEventTimestamp; Duration _lastPendingEventTimestamp;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
bool _isFlingGesture(VelocityEstimate estimate); bool _isFlingGesture(VelocityEstimate estimate);
Offset _getDeltaForDetails(Offset delta); Offset _getDeltaForDetails(Offset delta);
...@@ -149,6 +183,30 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -149,6 +183,30 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
@override
bool isPointerAllowed(PointerEvent event) {
if (_initialButtons == null) {
switch (event.buttons) {
case kPrimaryButton:
if (onDown == null &&
onStart == null &&
onUpdate == null &&
onEnd == null &&
onCancel == null)
return false;
break;
default:
return false;
}
} else {
// There can be multiple drags simultaneously. Their effects are combined.
if (event.buttons != _initialButtons) {
return false;
}
}
return super.isPointerAllowed(event);
}
@override @override
void addAllowedPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
...@@ -156,10 +214,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -156,10 +214,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == _DragState.ready) { if (_state == _DragState.ready) {
_state = _DragState.possible; _state = _DragState.possible;
_initialPosition = event.position; _initialPosition = event.position;
_initialButtons = event.buttons;
_pendingDragOffset = Offset.zero; _pendingDragOffset = Offset.zero;
_lastPendingEventTimestamp = event.timeStamp; _lastPendingEventTimestamp = event.timeStamp;
if (onDown != null) _checkDown();
invokeCallback<void>('onDown', () => onDown(DragDownDetails(globalPosition: _initialPosition)));
} else if (_state == _DragState.accepted) { } else if (_state == _DragState.accepted) {
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
...@@ -176,16 +234,19 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -176,16 +234,19 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
} }
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(event.pointer);
return;
}
final Offset delta = event.delta; final Offset delta = event.delta;
if (_state == _DragState.accepted) { if (_state == _DragState.accepted) {
if (onUpdate != null) { _checkUpdate(
invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails( sourceTimeStamp: event.timeStamp,
sourceTimeStamp: event.timeStamp, delta: _getDeltaForDetails(delta),
delta: _getDeltaForDetails(delta), primaryDelta: _getPrimaryValueFromOffset(delta),
primaryDelta: _getPrimaryValueFromOffset(delta), globalPosition: event.position,
globalPosition: event.position, );
)));
}
} else { } else {
_pendingDragOffset += delta; _pendingDragOffset += delta;
_lastPendingEventTimestamp = event.timeStamp; _lastPendingEventTimestamp = event.timeStamp;
...@@ -214,19 +275,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -214,19 +275,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
} }
_pendingDragOffset = Offset.zero; _pendingDragOffset = Offset.zero;
_lastPendingEventTimestamp = null; _lastPendingEventTimestamp = null;
if (onStart != null) { _checkStart(timestamp);
invokeCallback<void>('onStart', () => onStart(DragStartDetails( if (updateDelta != Offset.zero) {
sourceTimeStamp: timestamp, _checkUpdate(
globalPosition: _initialPosition,
)));
}
if (updateDelta != Offset.zero && onUpdate != null) {
invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
sourceTimeStamp: timestamp, sourceTimeStamp: timestamp,
delta: updateDelta, delta: updateDelta,
primaryDelta: _getPrimaryValueFromOffset(updateDelta), primaryDelta: _getPrimaryValueFromOffset(updateDelta),
globalPosition: _initialPosition + updateDelta, // Only adds delta for down behaviour globalPosition: _initialPosition + updateDelta, // Only adds delta for down behaviour
))); );
} }
} }
} }
...@@ -238,41 +294,101 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -238,41 +294,101 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override @override
void didStopTrackingLastPointer(int pointer) { void didStopTrackingLastPointer(int pointer) {
if (_state == _DragState.possible) { assert(_state != _DragState.ready);
resolve(GestureDisposition.rejected); switch(_state) {
_state = _DragState.ready; case _DragState.ready:
if (onCancel != null) break;
invokeCallback<void>('onCancel', onCancel);
return; case _DragState.possible:
resolve(GestureDisposition.rejected);
_checkCancel();
break;
case _DragState.accepted:
_checkEnd(pointer);
break;
} }
final bool wasAccepted = _state == _DragState.accepted; _velocityTrackers.clear();
_initialButtons = null;
_state = _DragState.ready; _state = _DragState.ready;
if (wasAccepted && onEnd != null) { }
final VelocityTracker tracker = _velocityTrackers[pointer];
assert(tracker != null);
final VelocityEstimate estimate = tracker.getVelocityEstimate(); void _checkDown() {
if (estimate != null && _isFlingGesture(estimate)) { assert(_initialButtons == kPrimaryButton);
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond) final DragDownDetails details = DragDownDetails(
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity); globalPosition: _initialPosition,
invokeCallback<void>('onEnd', () => onEnd(DragEndDetails( );
velocity: velocity, if (onDown != null)
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond), invokeCallback<void>('onDown', () => onDown(details));
)), debugReport: () { }
return '$estimate; fling at $velocity.';
}); void _checkStart(Duration timestamp) {
} else { assert(_initialButtons == kPrimaryButton);
invokeCallback<void>('onEnd', () => onEnd(DragEndDetails( final DragStartDetails details = DragStartDetails(
velocity: Velocity.zero, sourceTimeStamp: timestamp,
primaryVelocity: 0.0, globalPosition: _initialPosition,
)), debugReport: () { );
if (estimate == null) if (onStart != null)
return 'Could not estimate velocity.'; invokeCallback<void>('onStart', () => onStart(details));
return '$estimate; judged to not be a fling.'; }
});
} void _checkUpdate({
Duration sourceTimeStamp,
Offset delta,
double primaryDelta,
Offset globalPosition,
}) {
assert(_initialButtons == kPrimaryButton);
final DragUpdateDetails details = DragUpdateDetails(
sourceTimeStamp: sourceTimeStamp,
delta: delta,
primaryDelta: primaryDelta,
globalPosition: globalPosition,
);
if (onUpdate != null)
invokeCallback<void>('onUpdate', () => onUpdate(details));
}
void _checkEnd(int pointer) {
assert(_initialButtons == kPrimaryButton);
if (onEnd == null)
return;
final VelocityTracker tracker = _velocityTrackers[pointer];
assert(tracker != null);
DragEndDetails details;
void Function() debugReport;
final VelocityEstimate estimate = tracker.getVelocityEstimate();
if (estimate != null && _isFlingGesture(estimate)) {
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
details = DragEndDetails(
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
);
debugReport = () {
return '$estimate; fling at $velocity.';
};
} else {
details = DragEndDetails(
velocity: Velocity.zero,
primaryVelocity: 0.0,
);
debugReport = () {
if (estimate == null)
return 'Could not estimate velocity.';
return '$estimate; judged to not be a fling.';
};
} }
_velocityTrackers.clear(); invokeCallback<void>('onEnd', () => onEnd(details), debugReport: debugReport);
}
void _checkCancel() {
assert(_initialButtons == kPrimaryButton);
if (onCancel != null)
invokeCallback<void>('onCancel', onCancel);
} }
@override @override
......
...@@ -16,6 +16,10 @@ import 'tap.dart'; ...@@ -16,6 +16,10 @@ import 'tap.dart';
/// Signature for callback when the user has tapped the screen at the same /// Signature for callback when the user has tapped the screen at the same
/// location twice in quick succession. /// location twice in quick succession.
///
/// See also:
///
/// * [GestureDetector.onDoubleTap], which matches this signature.
typedef GestureDoubleTapCallback = void Function(); typedef GestureDoubleTapCallback = void Function();
/// Signature used by [MultiTapGestureRecognizer] for when a pointer that might /// Signature used by [MultiTapGestureRecognizer] for when a pointer that might
...@@ -60,13 +64,16 @@ class _TapTracker { ...@@ -60,13 +64,16 @@ class _TapTracker {
@required Duration doubleTapMinTime, @required Duration doubleTapMinTime,
}) : assert(doubleTapMinTime != null), }) : assert(doubleTapMinTime != null),
assert(event != null), assert(event != null),
assert(event.buttons != null),
pointer = event.pointer, pointer = event.pointer,
_initialPosition = event.position, _initialPosition = event.position,
initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime); _doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer; final int pointer;
final GestureArenaEntry entry; final GestureArenaEntry entry;
final Offset _initialPosition; final Offset _initialPosition;
final int initialButtons;
final _CountdownZoned _doubleTapMinTimeCountdown; final _CountdownZoned _doubleTapMinTimeCountdown;
bool _isTrackingPointer = false; bool _isTrackingPointer = false;
...@@ -93,10 +100,18 @@ class _TapTracker { ...@@ -93,10 +100,18 @@ class _TapTracker {
bool hasElapsedMinTime() { bool hasElapsedMinTime() {
return _doubleTapMinTimeCountdown.timeout; return _doubleTapMinTimeCountdown.timeout;
} }
bool hasSameButton(PointerDownEvent event) {
return event.buttons == initialButtons;
}
} }
/// Recognizes when the user has tapped the screen at the same location twice in /// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession. /// quick succession.
///
/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
///
class DoubleTapGestureRecognizer extends GestureRecognizer { class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps. /// Create a gesture recognizer for double taps.
/// ///
...@@ -126,26 +141,53 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -126,26 +141,53 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
// - The long timer between taps expires // - The long timer between taps expires
// - The gesture arena decides we have been rejected wholesale // - The gesture arena decides we have been rejected wholesale
/// Called when the user has tapped the screen at the same location twice in /// Called when the user has tapped the screen with a primary button at the
/// quick succession. /// same location twice in quick succession.
///
/// This triggers when the pointer stops contacting the device after the 2nd tap,
/// immediately after [onDoubleTapUp].
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
GestureDoubleTapCallback onDoubleTap; GestureDoubleTapCallback onDoubleTap;
Timer _doubleTapTimer; Timer _doubleTapTimer;
_TapTracker _firstTap; _TapTracker _firstTap;
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{}; final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@override
bool isPointerAllowed(PointerEvent event) {
if (_firstTap == null) {
switch (event.buttons) {
case kPrimaryButton:
if (onDoubleTap == null)
return false;
break;
default:
return false;
}
}
return super.isPointerAllowed(event);
}
@override @override
void addAllowedPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
if (_firstTap != null) { if (_firstTap != null) {
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) { if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps. // Ignore out-of-bounds second taps.
return; return;
} else if (!_firstTap.hasElapsedMinTime()) { } else if (!_firstTap.hasElapsedMinTime() || !_firstTap.hasSameButton(event)) {
// Restart when the second tap is too close to the first. // Restart when the second tap is too close to the first, or when buttons
// mismatch.
_reset(); _reset();
return addAllowedPointer(event); return _trackFirstTap(event);
} }
} }
_trackFirstTap(event);
}
void _trackFirstTap(PointerEvent event) {
_stopDoubleTapTimer(); _stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker( final _TapTracker tracker = _TapTracker(
event: event, event: event,
...@@ -235,8 +277,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -235,8 +277,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
tracker.entry.resolve(GestureDisposition.accepted); tracker.entry.resolve(GestureDisposition.accepted);
_freezeTracker(tracker); _freezeTracker(tracker);
_trackers.remove(tracker.pointer); _trackers.remove(tracker.pointer);
if (onDoubleTap != null) _checkUp(tracker.initialButtons);
invokeCallback<void>('onDoubleTap', onDoubleTap);
_reset(); _reset();
} }
...@@ -260,6 +301,12 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -260,6 +301,12 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
} }
} }
void _checkUp(int buttons) {
assert(buttons == kPrimaryButton);
if (onDoubleTap != null)
invokeCallback<void>('onDoubleTap', onDoubleTap);
}
@override @override
String get debugDescription => 'double tap'; String get debugDescription => 'double tap';
} }
......
...@@ -9,7 +9,7 @@ import 'constants.dart'; ...@@ -9,7 +9,7 @@ import 'constants.dart';
import 'events.dart'; import 'events.dart';
import 'recognizer.dart'; import 'recognizer.dart';
/// Details for [GestureTapDownCallback], such as position. /// Details for [GestureTapDownCallback], such as position
/// ///
/// See also: /// See also:
/// ///
...@@ -45,8 +45,6 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details); ...@@ -45,8 +45,6 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details);
/// * [GestureDetector.onTapUp], which receives this information. /// * [GestureDetector.onTapUp], which receives this information.
/// * [TapGestureRecognizer], which passes this information to one of its callbacks. /// * [TapGestureRecognizer], which passes this information to one of its callbacks.
class TapUpDetails { class TapUpDetails {
/// Creates details for a [GestureTapUpCallback].
///
/// The [globalPosition] argument must not be null. /// The [globalPosition] argument must not be null.
TapUpDetails({ this.globalPosition = Offset.zero }) TapUpDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null); : assert(globalPosition != null);
...@@ -95,14 +93,10 @@ typedef GestureTapCancelCallback = void Function(); ...@@ -95,14 +93,10 @@ typedef GestureTapCancelCallback = void Function();
/// pointer interactions during a tap sequence are not recognized as additional /// pointer interactions during a tap sequence are not recognized as additional
/// taps. For example, down-1, down-2, up-1, up-2 produces only one tap on up-1. /// taps. For example, down-1, down-2, up-1, up-2 produces only one tap on up-1.
/// ///
/// The lifecycle of events for a tap gesture is as follows: /// [TapGestureRecognizer] competes on pointer events of [kPrimaryButton] only
/// /// when it has at least one non-null `onTap*` callback, and events of
/// * [onTapDown], which triggers after a short timeout ([deadline]) even if the /// [kSecondaryButton] only when it has at least one non-null `onSecondaryTap*`
/// gesture has not won its arena yet. /// callback. If it has no callbacks, it is a no-op.
/// * [onTapUp] and [onTap], which trigger when the pointer is released if the
/// gesture wins the arena.
/// * [onTapCancel], which triggers instead of [onTapUp] and [onTap] in the case
/// of the gesture not winning the arena.
/// ///
/// See also: /// See also:
/// ///
...@@ -112,22 +106,25 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -112,22 +106,25 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a tap gesture recognizer. /// Creates a tap gesture recognizer.
TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner); TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
/// A pointer that might cause a tap has contacted the screen at a particular /// A pointer that might cause a tap of a primary button has contacted the
/// location. /// screen at a particular location.
/// ///
/// This triggers before the gesture has won the arena, after a short timeout /// This triggers once a short timeout ([deadline]) has elapsed, or once
/// ([deadline]). /// the gestures has won the arena, whichever comes first.
/// ///
/// If the gesture doesn't win the arena, [onTapCancel] is called next. /// If the gesture doesn't win the arena, [onTapCancel] is called next.
/// Otherwise, [onTapUp] is called next. /// Otherwise, [onTapUp] is called next.
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryTapDown], a similar callback but for a secondary button.
/// * [TapDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onTapDown], which exposes this callback. /// * [GestureDetector.onTapDown], which exposes this callback.
GestureTapDownCallback onTapDown; GestureTapDownCallback onTapDown;
/// A pointer that will trigger a tap has stopped contacting the screen at a /// A pointer that will trigger a tap of a primary button has stopped
/// particular location. /// contacting the screen at a particular location.
/// ///
/// This triggers once the gesture has won the arena, immediately before /// This triggers once the gesture has won the arena, immediately before
/// [onTap]. /// [onTap].
...@@ -136,11 +133,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -136,11 +133,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [GestureDetector.onTapUp], which exposes this callback. /// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryTapUp], a similar callback but for a secondary button.
/// * [TapUpDetails], which is passed as an argument to this callback. /// * [TapUpDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onTapUp], which exposes this callback.
GestureTapUpCallback onTapUp; GestureTapUpCallback onTapUp;
/// A tap has occurred. /// A tap of a primary button has occurred.
/// ///
/// This triggers once the gesture has won the arena, immediately after /// This triggers once the gesture has won the arena, immediately after
/// [onTapUp]. /// [onTapUp].
...@@ -149,6 +148,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -149,6 +148,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onTapUp], which has the same timing but with details.
/// * [GestureDetector.onTap], which exposes this callback. /// * [GestureDetector.onTap], which exposes this callback.
GestureTapCallback onTap; GestureTapCallback onTap;
...@@ -161,37 +162,121 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -161,37 +162,121 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryTapCancel], a similar callback but for a secondary button.
/// * [GestureDetector.onTapCancel], which exposes this callback. /// * [GestureDetector.onTapCancel], which exposes this callback.
GestureTapCancelCallback onTapCancel; GestureTapCancelCallback onTapCancel;
/// A pointer that might cause a tap of a secondary button has contacted the
/// screen at a particular location.
///
/// This triggers once a short timeout ([deadline]) has elapsed, or once
/// the gestures has won the arena, whichever comes first.
///
/// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called next.
/// Otherwise, [onSecondaryTapUp] is called next.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onPrimaryTapDown], a similar callback but for a primary button.
/// * [TapDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onSecondaryTapDown], which exposes this callback.
GestureTapDownCallback onSecondaryTapDown;
/// A pointer that will trigger a tap of a secondary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers once the gesture has won the arena.
///
/// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called
/// instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onPrimaryTapUp], a similar callback but for a primary button.
/// * [TapUpDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onSecondaryTapUp], which exposes this callback.
GestureTapUpCallback onSecondaryTapUp;
/// The pointer that previously triggered [onSecondaryTapDown] will not end up
/// causing a tap.
///
/// This triggers if the gesture loses the arena.
///
/// If the gesture wins the arena, [onSecondaryTapUp] is called instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onPrimaryTapCancel], a similar callback but for a primary button.
/// * [GestureDetector.onTapCancel], which exposes this callback.
GestureTapCancelCallback onSecondaryTapCancel;
bool _sentTapDown = false; bool _sentTapDown = false;
bool _wonArenaForPrimaryPointer = false; bool _wonArenaForPrimaryPointer = false;
Offset _finalPosition; Offset _finalPosition;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
@override
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {
case kPrimaryButton:
if (onTapDown == null &&
onTap == null &&
onTapUp == null &&
onTapCancel == null)
return false;
break;
case kSecondaryButton:
if (onSecondaryTapDown == null &&
onSecondaryTapUp == null &&
onSecondaryTapCancel == null)
return false;
break;
default:
return false;
}
return super.isPointerAllowed(event);
}
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
// `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
// because `acceptGesture` might be called before `handlePrimaryPointer`,
// which relies on `_initialButtons` to create `TapDownDetails`.
_initialButtons = event.buttons;
}
@override @override
void handlePrimaryPointer(PointerEvent event) { void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) { if (event is PointerUpEvent) {
_finalPosition = event.position; _finalPosition = event.position;
if (_wonArenaForPrimaryPointer) { _checkUp();
resolve(GestureDisposition.accepted);
_checkUp();
}
} else if (event is PointerCancelEvent) { } else if (event is PointerCancelEvent) {
if (_sentTapDown && onTapCancel != null) { resolve(GestureDisposition.rejected);
invokeCallback<void>('onTapCancel', onTapCancel); if (_sentTapDown) {
_checkCancel('');
} }
_reset(); _reset();
} else if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
} }
} }
@override @override
void resolve(GestureDisposition disposition) { void resolve(GestureDisposition disposition) {
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) { if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
// This can happen if the superclass decides the primary pointer // This can happen if the gesture has been canceled. For example, when
// exceeded the touch slop, or if the recognizer is disposed. // the pointer has exceeded the touch slop, the buttons have been changed,
// or if the recognizer is disposed.
assert(_sentTapDown); assert(_sentTapDown);
if (onTapCancel != null) _checkCancel('spontaneous ');
invokeCallback<void>('spontaneous onTapCancel', onTapCancel);
_reset(); _reset();
} }
super.resolve(disposition); super.resolve(disposition);
...@@ -218,27 +303,70 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -218,27 +303,70 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
if (pointer == primaryPointer) { if (pointer == primaryPointer) {
// Another gesture won the arena. // Another gesture won the arena.
assert(state != GestureRecognizerState.possible); assert(state != GestureRecognizerState.possible);
if (_sentTapDown && onTapCancel != null) if (_sentTapDown)
invokeCallback<void>('forced onTapCancel', onTapCancel); _checkCancel('forced ');
_reset(); _reset();
} }
} }
void _checkDown() { void _checkDown() {
if (!_sentTapDown) { if (_sentTapDown) {
if (onTapDown != null) return;
invokeCallback<void>('onTapDown', () { onTapDown(TapDownDetails(globalPosition: initialPosition)); }); }
_sentTapDown = true; final TapDownDetails details = TapDownDetails(
globalPosition: initialPosition,
);
switch (_initialButtons) {
case kPrimaryButton:
if (onTapDown != null)
invokeCallback<void>('onTapDown', () => onTapDown(details));
break;
case kSecondaryButton:
if (onSecondaryTapDown != null)
invokeCallback<void>('onSecondaryTapDown',
() => onSecondaryTapDown(details));
break;
default:
} }
_sentTapDown = true;
} }
void _checkUp() { void _checkUp() {
if (_finalPosition != null) { if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
if (onTapUp != null) return;
invokeCallback<void>('onTapUp', () { onTapUp(TapUpDetails(globalPosition: _finalPosition)); }); }
if (onTap != null) final TapUpDetails details = TapUpDetails(
invokeCallback<void>('onTap', onTap); globalPosition: _finalPosition,
_reset(); );
switch (_initialButtons) {
case kPrimaryButton:
if (onTapUp != null)
invokeCallback<void>('onTapUp', () => onTapUp(details));
if (onTap != null)
invokeCallback<void>('onTap', onTap);
break;
case kSecondaryButton:
if (onSecondaryTapUp != null)
invokeCallback<void>('onSecondaryTapUp',
() => onSecondaryTapUp(details));
break;
default:
}
_reset();
}
void _checkCancel(String note) {
switch (_initialButtons) {
case kPrimaryButton:
if (onTapCancel != null)
invokeCallback<void>('${note}onTapCancel', onTapCancel);
break;
case kSecondaryButton:
if (onSecondaryTapCancel != null)
invokeCallback<void>('${note}onSecondaryTapCancel',
onSecondaryTapCancel);
break;
default:
} }
} }
...@@ -246,6 +374,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -246,6 +374,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
_sentTapDown = false; _sentTapDown = false;
_wonArenaForPrimaryPointer = false; _wonArenaForPrimaryPointer = false;
_finalPosition = null; _finalPosition = null;
_initialButtons = null;
} }
@override @override
...@@ -257,5 +386,6 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -257,5 +386,6 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena')); properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null)); properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null));
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down')); properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
// TODO(tongmu): Add property _initialButtons and update related tests
} }
} }
...@@ -166,6 +166,9 @@ class GestureDetector extends StatelessWidget { ...@@ -166,6 +166,9 @@ class GestureDetector extends StatelessWidget {
this.onTapUp, this.onTapUp,
this.onTap, this.onTap,
this.onTapCancel, this.onTapCancel,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.onSecondaryTapCancel,
this.onDoubleTap, this.onDoubleTap,
this.onLongPress, this.onLongPress,
this.onLongPressStart, this.onLongPressStart,
...@@ -229,28 +232,37 @@ class GestureDetector extends StatelessWidget { ...@@ -229,28 +232,37 @@ class GestureDetector extends StatelessWidget {
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget child; final Widget child;
/// A pointer that might cause a tap has contacted the screen at a particular /// A pointer that might cause a tap with a primary button has contacted the
/// location. /// screen at a particular location.
/// ///
/// This is called after a short timeout, even if the winning gesture has not /// This is called after a short timeout, even if the winning gesture has not
/// yet been selected. If the tap gesture wins, [onTapUp] will be called, /// yet been selected. If the tap gesture wins, [onTapUp] will be called,
/// otherwise [onTapCancel] will be called. /// otherwise [onTapCancel] will be called.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapDownCallback onTapDown; final GestureTapDownCallback onTapDown;
/// A pointer that will trigger a tap has stopped contacting the screen at a /// A pointer that will trigger a tap with a primary button has stopped
/// particular location. /// contacting the screen at a particular location.
/// ///
/// This triggers immediately before [onTap] in the case of the tap gesture /// This triggers immediately before [onTap] in the case of the tap gesture
/// winning. If the tap gesture did not win, [onTapCancel] is called instead. /// winning. If the tap gesture did not win, [onTapCancel] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapUpCallback onTapUp; final GestureTapUpCallback onTapUp;
/// A tap has occurred. /// A tap with a primary button has occurred.
/// ///
/// This triggers when the tap gesture wins. If the tap gesture did not win, /// This triggers when the tap gesture wins. If the tap gesture did not win,
/// [onTapCancel] is called instead. /// [onTapCancel] is called instead.
/// ///
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onTapUp], which is called at the same time but includes details /// * [onTapUp], which is called at the same time but includes details
/// regarding the pointer position. /// regarding the pointer position.
final GestureTapCallback onTap; final GestureTapCallback onTap;
...@@ -260,104 +272,222 @@ class GestureDetector extends StatelessWidget { ...@@ -260,104 +272,222 @@ class GestureDetector extends StatelessWidget {
/// ///
/// This is called after [onTapDown], and instead of [onTapUp] and [onTap], if /// This is called after [onTapDown], and instead of [onTapUp] and [onTap], if
/// the tap gesture did not win. /// the tap gesture did not win.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback onTapCancel; final GestureTapCancelCallback onTapCancel;
/// The user has tapped the screen at the same location twice in quick /// A pointer that might cause a tap with a secondary button has contacted the
/// succession. /// screen at a particular location.
///
/// This is called after a short timeout, even if the winning gesture has not
/// yet been selected. If the tap gesture wins, [onSecondaryTapUp] will be
/// called, otherwise [onSecondaryTapCancel] will be called.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapDownCallback onSecondaryTapDown;
/// A pointer that will trigger a tap with a secondary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers in the case of the tap gesture winning. If the tap gesture
/// did not win, [onSecondaryTapCancel] is called instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapUpCallback onSecondaryTapUp;
/// The pointer that previously triggered [onSecondaryTapDown] will not end up
/// causing a tap.
///
/// This is called after [onSecondaryTapDown], and instead of
/// [onSecondaryTapUp], if the tap gesture did not win.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapCancelCallback onSecondaryTapCancel;
/// The user has tapped the screen with a primary button at the same location
/// twice in quick succession.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCallback onDoubleTap; final GestureTapCallback onDoubleTap;
/// Called when a long press gesture has been recognized. /// Called when a long press gesture with a primary button has been recognized.
/// ///
/// Triggered when a pointer has remained in contact with the screen at the /// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time. /// same location for a long period of time.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPressStart], which has the same timing but has data for the /// * [kPrimaryButton], the button this callback responds to.
/// press location. /// * [onLongPressStart], which has the same timing but has gesture details.
final GestureLongPressCallback onLongPress; final GestureLongPressCallback onLongPress;
/// Callback for long press start with gesture location. /// Called when a long press gesture with a primary button has been recognized.
/// ///
/// Triggered when a pointer has remained in contact with the screen at the /// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time. /// same location for a long period of time.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPress], which has the same timing but without the location data. /// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPress], which has the same timing but without the gesture details.
final GestureLongPressStartCallback onLongPressStart; final GestureLongPressStartCallback onLongPressStart;
/// A pointer has been drag-moved after a long press. /// A pointer has been drag-moved after a long press with a primary button.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureLongPressMoveUpdateCallback onLongPressMoveUpdate; final GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
/// A pointer that has triggered a long-press has stopped contacting the screen. /// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPressEnd], which has the same timing but has data for the up /// * [kPrimaryButton], the button this callback responds to.
/// gesture location. /// * [onLongPressEnd], which has the same timing but has gesture details.
final GestureLongPressUpCallback onLongPressUp; final GestureLongPressUpCallback onLongPressUp;
/// A pointer that has triggered a long-press has stopped contacting the screen. /// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
/// ///
/// See also: /// See also:
/// ///
/// * [onLongPressUp], which has the same timing but without the location data. /// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressUp], which has the same timing but without the gesture
/// details.
final GestureLongPressEndCallback onLongPressEnd; final GestureLongPressEndCallback onLongPressEnd;
/// A pointer has contacted the screen and might begin to move vertically. /// A pointer has contacted the screen with a primary button and might begin
/// to move vertically.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback onVerticalDragDown; final GestureDragDownCallback onVerticalDragDown;
/// A pointer has contacted the screen and has begun to move vertically. /// A pointer has contacted the screen with a primary button and has begun to
/// move vertically.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback onVerticalDragStart; final GestureDragStartCallback onVerticalDragStart;
/// A pointer that is in contact with the screen and moving vertically has /// A pointer that is in contact with the screen with a primary button and
/// moved in the vertical direction. /// moving vertically has moved in the vertical direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback onVerticalDragUpdate; final GestureDragUpdateCallback onVerticalDragUpdate;
/// A pointer that was previously in contact with the screen and moving /// A pointer that was previously in contact with the screen with a primary
/// vertically is no longer in contact with the screen and was moving at a /// button and moving vertically is no longer in contact with the screen and
/// specific velocity when it stopped contacting the screen. /// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onVerticalDragEnd; final GestureDragEndCallback onVerticalDragEnd;
/// The pointer that previously triggered [onVerticalDragDown] did not /// The pointer that previously triggered [onVerticalDragDown] did not
/// complete. /// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback onVerticalDragCancel; final GestureDragCancelCallback onVerticalDragCancel;
/// A pointer has contacted the screen and might begin to move horizontally. /// A pointer has contacted the screen with a primary button and might begin
/// to move horizontally.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback onHorizontalDragDown; final GestureDragDownCallback onHorizontalDragDown;
/// A pointer has contacted the screen and has begun to move horizontally. /// A pointer has contacted the screen with a primary button and has begun to
/// move horizontally.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback onHorizontalDragStart; final GestureDragStartCallback onHorizontalDragStart;
/// A pointer that is in contact with the screen and moving horizontally has /// A pointer that is in contact with the screen with a primary button and
/// moved in the horizontal direction. /// moving horizontally has moved in the horizontal direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback onHorizontalDragUpdate; final GestureDragUpdateCallback onHorizontalDragUpdate;
/// A pointer that was previously in contact with the screen and moving /// A pointer that was previously in contact with the screen with a primary
/// horizontally is no longer in contact with the screen and was moving at a /// button and moving horizontally is no longer in contact with the screen and
/// specific velocity when it stopped contacting the screen. /// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onHorizontalDragEnd; final GestureDragEndCallback onHorizontalDragEnd;
/// The pointer that previously triggered [onHorizontalDragDown] did not /// The pointer that previously triggered [onHorizontalDragDown] did not
/// complete. /// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback onHorizontalDragCancel; final GestureDragCancelCallback onHorizontalDragCancel;
/// A pointer has contacted the screen and might begin to move. /// A pointer has contacted the screen with a primary button and might begin
/// to move.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback onPanDown; final GestureDragDownCallback onPanDown;
/// A pointer has contacted the screen and has begun to move. /// A pointer has contacted the screen with a primary button and has begun to
/// move.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback onPanStart; final GestureDragStartCallback onPanStart;
/// A pointer that is in contact with the screen and moving has moved again. /// A pointer that is in contact with the screen with a primary button and
/// moving has moved again.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback onPanUpdate; final GestureDragUpdateCallback onPanUpdate;
/// A pointer that was previously in contact with the screen and moving /// A pointer that was previously in contact with the screen with a primary
/// is no longer in contact with the screen and was moving at a specific /// button and moving is no longer in contact with the screen and was moving
/// velocity when it stopped contacting the screen. /// at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onPanEnd; final GestureDragEndCallback onPanEnd;
/// The pointer that previously triggered [onPanDown] did not complete. /// The pointer that previously triggered [onPanDown] did not complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback onPanCancel; final GestureDragCancelCallback onPanCancel;
/// The pointers in contact with the screen have established a focal point and /// The pointers in contact with the screen have established a focal point and
...@@ -440,7 +570,15 @@ class GestureDetector extends StatelessWidget { ...@@ -440,7 +570,15 @@ class GestureDetector extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{}; final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) { if (
onTapDown != null ||
onTapUp != null ||
onTap != null ||
onTapCancel != null ||
onSecondaryTapDown != null ||
onSecondaryTapUp != null ||
onSecondaryTapCancel != null
) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this), () => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) { (TapGestureRecognizer instance) {
...@@ -448,7 +586,10 @@ class GestureDetector extends StatelessWidget { ...@@ -448,7 +586,10 @@ class GestureDetector extends StatelessWidget {
..onTapDown = onTapDown ..onTapDown = onTapDown
..onTapUp = onTapUp ..onTapUp = onTapUp
..onTap = onTap ..onTap = onTap
..onTapCancel = onTapCancel; ..onTapCancel = onTapCancel
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel;
}, },
); );
} }
...@@ -797,8 +938,14 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -797,8 +938,14 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
void _handleSemanticsLongPress() { void _handleSemanticsLongPress() {
final LongPressGestureRecognizer recognizer = _recognizers[LongPressGestureRecognizer]; final LongPressGestureRecognizer recognizer = _recognizers[LongPressGestureRecognizer];
assert(recognizer != null); assert(recognizer != null);
if (recognizer.onLongPressStart != null)
recognizer.onLongPressStart(const LongPressStartDetails());
if (recognizer.onLongPress != null) if (recognizer.onLongPress != null)
recognizer.onLongPress(); recognizer.onLongPress();
if (recognizer.onLongPressEnd != null)
recognizer.onLongPressEnd(const LongPressEndDetails());
if (recognizer.onLongPressUp != null)
recognizer.onLongPressUp();
} }
void _handleSemanticsHorizontalDragUpdate(DragUpdateDetails updateDetails) { void _handleSemanticsHorizontalDragUpdate(DragUpdateDetails updateDetails) {
......
...@@ -147,7 +147,8 @@ void main() { ...@@ -147,7 +147,8 @@ void main() {
}); });
test('TapGestureRecognizer _sentTapDown toString', () { test('TapGestureRecognizer _sentTapDown toString', () {
final TapGestureRecognizer tap = TapGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer()
..onTap = () {}; // Add a callback so that event can be added
expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)')); expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)'));
const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0)); const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
tap.addPointer(event); tap.addPointer(event);
......
...@@ -75,7 +75,7 @@ void main() { ...@@ -75,7 +75,7 @@ void main() {
position: Offset(25.0, 25.0), position: Offset(25.0, 25.0),
); );
// Down/up pair 5: normal tap sequence identical to pair 1 with different pointer // Down/up pair 5: normal tap sequence identical to pair 1
const PointerDownEvent down5 = PointerDownEvent( const PointerDownEvent down5 = PointerDownEvent(
pointer: 5, pointer: 5,
position: Offset(10.0, 10.0), position: Offset(10.0, 10.0),
...@@ -86,6 +86,18 @@ void main() { ...@@ -86,6 +86,18 @@ void main() {
position: Offset(11.0, 9.0), position: Offset(11.0, 9.0),
); );
// Down/up pair 6: normal tap sequence close to pair 1 but on secondary button
const PointerDownEvent down6 = PointerDownEvent(
pointer: 6,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const PointerUpEvent up6 = PointerUpEvent(
pointer: 6,
position: Offset(11.0, 9.0),
);
testGesture('Should recognize double tap', (GestureTester tester) { testGesture('Should recognize double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer(); final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
...@@ -606,4 +618,183 @@ void main() { ...@@ -606,4 +618,183 @@ void main() {
tap.dispose(); tap.dispose();
}); });
group('Enforce consistent-button restriction:', () {
testGesture('Button change should interrupt existing sequence', (GestureTester tester) {
// Down1 -> down6 (different button from 1) -> down2 (same button as 1)
// Down1 and down2 could've been a double tap, but is interrupted by down 6.
const Duration interval = Duration(milliseconds: 100);
assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
tester.async.elapse(interval);
tap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(interval);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose();
});
testGesture('Button change should start a valid sequence', (GestureTester tester) {
// Down6 -> down1 (different button from 6) -> down2 (same button as 1)
const Duration interval = Duration(milliseconds: 100);
assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(interval);
tap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(interval);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isTrue);
tap.dispose();
});
});
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that double tap recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
TapGestureRecognizer tapPrimary;
TapGestureRecognizer tapSecondary;
DoubleTapGestureRecognizer doubleTap;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognized.add('doubleTap');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
doubleTap.dispose();
});
testGesture('A primary double tap recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
doubleTap.addPointer(down6);
tapSecondary.addPointer(down6);
tester.closeArena(down6.pointer);
tester.route(down6);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary double tap recognizer forms competion with a primary tap recognizer', (GestureTester tester) {
doubleTap.addPointer(down1);
tapPrimary.addPointer(down1);
tester.closeArena(down1.pointer);
tester.route(down1);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary double tap should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognized.add('primary');
};
// Down/up pair 7: normal tap sequence close to pair 6
const PointerDownEvent down7 = PointerDownEvent(
pointer: 7,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const PointerUpEvent up7 = PointerUpEvent(
pointer: 7,
position: Offset(11.0, 9.0),
);
doubleTap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(const Duration(milliseconds: 100));
doubleTap.addPointer(down7);
tester.closeArena(7);
tester.route(down7);
tester.route(up7);
expect(recognized, <String>[]);
recognized.clear();
doubleTap.dispose();
});
} }
...@@ -13,7 +13,7 @@ void main() { ...@@ -13,7 +13,7 @@ void main() {
testGesture('Should recognize pan', (GestureTester tester) { testGesture('Should recognize pan', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer(); final PanGestureRecognizer pan = PanGestureRecognizer();
final TapGestureRecognizer tap = TapGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer()..onTap = () {};
bool didStartPan = false; bool didStartPan = false;
pan.onStart = (_) { pan.onStart = (_) {
...@@ -81,7 +81,8 @@ void main() { ...@@ -81,7 +81,8 @@ void main() {
testGesture('Should report most recent point to onStart by default', (GestureTester tester) { testGesture('Should report most recent point to onStart by default', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer(); final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..onStart = (_) {};
Offset positionAtOnStart; Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) { drag.onStart = (DragStartDetails details) {
...@@ -103,9 +104,9 @@ void main() { ...@@ -103,9 +104,9 @@ void main() {
}); });
testGesture('Should report most recent point to onStart with a start configuration', (GestureTester tester) { testGesture('Should report most recent point to onStart with a start configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
HorizontalDragGestureRecognizer(); final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer(); ..onStart = (_) {};
Offset positionAtOnStart; Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) { drag.onStart = (DragStartDetails details) {
...@@ -218,9 +219,11 @@ void main() { ...@@ -218,9 +219,11 @@ void main() {
// TODO(jslavitz): Revert these tests. // TODO(jslavitz): Revert these tests.
testGesture('Should report initial down point to onStart with a down configuration', (GestureTester tester) { testGesture('Should report initial down point to onStart with a down configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; ..dragStartBehavior = DragStartBehavior.down;
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down
..onStart = (_) {};
Offset positionAtOnStart; Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) { drag.onStart = (DragStartDetails details) {
...@@ -596,4 +599,252 @@ void main() { ...@@ -596,4 +599,252 @@ void main() {
drag.dispose(); drag.dispose();
}); });
group('Enforce consistent-button restriction:', () {
PanGestureRecognizer pan;
TapGestureRecognizer tap;
final List<String> logs = <String>[];
setUp(() {
tap = TapGestureRecognizer()
..onTap = () {}; // Need a callback to enable competition
pan = PanGestureRecognizer()
..onStart = (DragStartDetails details) {
logs.add('start');
}
..onDown = (DragDownDetails details) {
logs.add('down');
}
..onUpdate = (DragUpdateDetails details) {
logs.add('update');
}
..onCancel = () {
logs.add('cancel');
}
..onEnd = (DragEndDetails details) {
logs.add('end');
};
});
tearDown(() {
pan.dispose();
tap.dispose();
logs.clear();
});
testGesture('Button change before acceptance should lead to immediate cancel', (GestureTester tester) {
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(5);
tester.route(down);
expect(logs, <String>['down']);
pointer.buttons = kSecondaryButton;
// Move out of slop so make sure button changes takes priority over slops
tester.route(pointer.move(const Offset(30.0, 30.0)));
expect(logs, <String>['down', 'cancel']);
tester.route(pointer.up());
});
testGesture('Button change before acceptance should not prevent the next drag', (GestureTester tester) {
{ // First drag (which is canceled)
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
pointer.buttons = kSecondaryButton;
tester.route(pointer.move(const Offset(10.0, 10.0)));
tester.route(pointer.up());
expect(logs, <String>['down', 'cancel']);
}
logs.clear();
final TestPointer pointer2 = TestPointer(6, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 10.0));
pan.addPointer(down2);
tap.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
expect(logs, <String>['down']);
tester.route(pointer2.move(const Offset(30.0, 30.0)));
expect(logs, <String>['down', 'start']);
tester.route(pointer2.up());
expect(logs, <String>['down', 'start', 'end']);
});
testGesture('Button change after acceptance should lead to immediate end', (GestureTester tester) {
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(logs, <String>['down']);
tester.route(pointer.move(const Offset(30.0, 30.0)));
expect(logs, <String>['down', 'start']);
pointer.buttons = kSecondaryButton;
tester.route(pointer.move(const Offset(30.0, 30.0)));
expect(logs, <String>['down', 'start', 'end']);
// Make sure no further updates are sent
tester.route(pointer.move(const Offset(50.0, 50.0)));
expect(logs, <String>['down', 'start', 'end']);
tester.route(pointer.up());
});
testGesture('Button change after acceptance should not prevent the next drag', (GestureTester tester) {
{ // First drag (which is canceled)
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.route(pointer.move(const Offset(30.0, 30.0)));
pointer.buttons = kSecondaryButton;
tester.route(pointer.move(const Offset(30.0, 31.0)));
tester.route(pointer.up());
expect(logs, <String>['down', 'start', 'end']);
}
logs.clear();
final TestPointer pointer2 = TestPointer(6, PointerDeviceKind.mouse, kPrimaryButton);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 10.0));
pan.addPointer(down2);
tap.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
expect(logs, <String>['down']);
tester.route(pointer2.move(const Offset(30.0, 30.0)));
expect(logs, <String>['down', 'start']);
tester.route(pointer2.up());
expect(logs, <String>['down', 'start', 'end']);
});
});
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that drag recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
TapGestureRecognizer tapPrimary;
TapGestureRecognizer tapSecondary;
PanGestureRecognizer pan;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
pan = PanGestureRecognizer()
..onStart = (_) {
recognized.add('drag');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
pan.dispose();
});
testGesture('A primary pan recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
final TestPointer pointer = TestPointer(
1,
PointerDeviceKind.touch,
kSecondaryButton,
);
final PointerDownEvent down = pointer.down(const Offset(10, 10));
pan.addPointer(down);
tapSecondary.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary pan recognizer forms competion with a primary tap recognizer', (GestureTester tester) {
final TestPointer pointer = TestPointer(
1,
PointerDeviceKind.touch,
kPrimaryButton,
);
final PointerDownEvent down = pointer.down(const Offset(10, 10));
pan.addPointer(down);
tapPrimary.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>[]);
tester.route(pointer.up());
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary drag should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final TapGestureRecognizer tap = TapGestureRecognizer()
..onTap = () {}; // Need a listener to enable competetion.
final PanGestureRecognizer pan = PanGestureRecognizer()
..onDown = (DragDownDetails details) {
recognized.add('primaryDown');
}
..onStart = (DragStartDetails details) {
recognized.add('primaryStart');
}
..onUpdate = (DragUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onEnd = (DragEndDetails details) {
recognized.add('primaryEnd');
}
..onCancel = () {
recognized.add('primaryCancel');
};
final TestPointer pointer = TestPointer(
5,
PointerDeviceKind.touch,
kSecondaryButton,
);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(5);
tester.route(down);
tester.route(pointer.move(const Offset(20.0, 30.0)));
tester.route(pointer.move(const Offset(20.0, 25.0)));
tester.route(pointer.up());
expect(recognized, <String>[]);
recognized.clear();
pan.dispose();
tap.dispose();
recognized.clear();
});
} }
...@@ -20,6 +20,20 @@ void main() { ...@@ -20,6 +20,20 @@ void main() {
expect(nthStylusButton(2), kSecondaryStylusButton); expect(nthStylusButton(2), kSecondaryStylusButton);
}); });
testGesture('smallestButton tests', (GestureTester tester) {
expect(smallestButton(0x0), equals(0x0));
expect(smallestButton(0x1), equals(0x1));
expect(smallestButton(0x200), equals(0x200));
expect(smallestButton(0x220), equals(0x20));
});
testGesture('isSingleButton tests', (GestureTester tester) {
expect(isSingleButton(0x0), isFalse);
expect(isSingleButton(0x1), isTrue);
expect(isSingleButton(0x200), isTrue);
expect(isSingleButton(0x220), isFalse);
});
group('Default values of PointerEvents:', () { group('Default values of PointerEvents:', () {
// Some parameters are intentionally set to a non-trivial value. // Some parameters are intentionally set to a non-trivial value.
......
...@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
import 'gesture_tester.dart'; import 'gesture_tester.dart';
// Down/move/up pair 1: normal tap sequence
const PointerDownEvent down = PointerDownEvent( const PointerDownEvent down = PointerDownEvent(
pointer: 5, pointer: 5,
position: Offset(10, 10), position: Offset(10, 10),
...@@ -22,6 +23,29 @@ const PointerMoveEvent move = PointerMoveEvent( ...@@ -22,6 +23,29 @@ const PointerMoveEvent move = PointerMoveEvent(
position: Offset(100, 200), position: Offset(100, 200),
); );
// Down/up pair 2: normal tap sequence far away from pair 1
const PointerDownEvent down2 = PointerDownEvent(
pointer: 6,
position: Offset(10, 10),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 6,
position: Offset(11, 9),
);
// Down/up pair 3: tap sequence with secondary button
const PointerDownEvent down3 = PointerDownEvent(
pointer: 7,
position: Offset(30, 30),
buttons: kSecondaryButton,
);
const PointerUpEvent up3 = PointerUpEvent(
pointer: 7,
position: Offset(31, 29),
);
void main() { void main() {
setUp(ensureGestureBinding); setUp(ensureGestureBinding);
...@@ -187,6 +211,48 @@ void main() { ...@@ -187,6 +211,48 @@ void main() {
longPress.dispose(); longPress.dispose();
}); });
testGesture('Should not recognize long press with more than one buttons', (GestureTester tester) {
longPress.addPointer(const PointerDownEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton | kMiddleMouseButton,
position: Offset(10, 10),
));
tester.closeArena(5);
expect(longPressDown, isFalse);
tester.route(down);
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(longPressDown, isFalse);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
longPress.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
tester.route(down);
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
tester.route(const PointerMoveEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kMiddleMouseButton,
position: Offset(10, 10),
));
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 700));
expect(longPressDown, isFalse);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
});
}); });
group('long press drag', () { group('long press drag', () {
...@@ -279,6 +345,107 @@ void main() { ...@@ -279,6 +345,107 @@ void main() {
}); });
}); });
group('Enforce consistent-button restriction:', () {
// In sequence between `down` and `up` but with buttons changed
const PointerMoveEvent moveR = PointerMoveEvent(
pointer: 5,
buttons: kSecondaryButton,
position: Offset(10, 10),
);
final List<String> recognized = <String>[];
LongPressGestureRecognizer longPress;
setUp(() {
longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('start');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('end');
};
});
tearDown(() {
longPress.dispose();
recognized.clear();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
expect(recognized, <String>[]);
});
testGesture('Buttons change before acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
recognized.clear();
// Second press
longPress.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
testGesture('Should cancel long press when buttons change after acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(moveR);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>[]);
});
testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(moveR);
tester.route(up);
recognized.clear();
// Second press
longPress.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
});
testGesture('Can filter long press based on device kind', (GestureTester tester) { testGesture('Can filter long press based on device kind', (GestureTester tester) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse); final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse);
...@@ -318,4 +485,107 @@ void main() { ...@@ -318,4 +485,107 @@ void main() {
mouseLongPress.dispose(); mouseLongPress.dispose();
}); });
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that long press recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
TapGestureRecognizer tapPrimary;
TapGestureRecognizer tapSecondary;
LongPressGestureRecognizer longPress;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
longPress = LongPressGestureRecognizer()
..onLongPressStart = (_) {
recognized.add('longPress');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
longPress.dispose();
});
testGesture('A primary long press recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
longPress.addPointer(down3);
tapSecondary.addPointer(down3);
tester.closeArena(down3.pointer);
tester.route(down3);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary long press recognizer forms competion with a primary tap recognizer', (GestureTester tester) {
longPress.addPointer(down);
tapPrimary.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary long press should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPress = () {
recognized.add('primary');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
}
..onLongPressUp = () {
recognized.add('primaryUp');
};
const PointerDownEvent down2 = PointerDownEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(30.0, 30.0),
);
const PointerMoveEvent move2 = PointerMoveEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(100, 200),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 2,
position: Offset(100, 201),
);
longPress.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(move2);
tester.route(up2);
expect(recognized, <String>[]);
longPress.dispose();
recognized.clear();
});
} }
...@@ -73,6 +73,18 @@ void main() { ...@@ -73,6 +73,18 @@ void main() {
position: Offset(22.0, 22.0), position: Offset(22.0, 22.0),
); );
// Down/up sequence 5: tap sequence with secondary button
const PointerDownEvent down5 = PointerDownEvent(
pointer: 5,
position: Offset(20.0, 20.0),
buttons: kSecondaryButton,
);
const PointerUpEvent up5 = PointerUpEvent(
pointer: 5,
position: Offset(20.0, 20.0),
);
testGesture('Should recognize tap', (GestureTester tester) { testGesture('Should recognize tap', (GestureTester tester) {
final TapGestureRecognizer tap = TapGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer();
...@@ -439,6 +451,48 @@ void main() { ...@@ -439,6 +451,48 @@ void main() {
tap.dispose(); tap.dispose();
}); });
testGesture('PointerCancelEvent after exceeding deadline cancels tap', (GestureTester tester) {
const PointerDownEvent down = PointerDownEvent(
pointer: 5,
position: Offset(10.0, 10.0),
);
const PointerCancelEvent cancel = PointerCancelEvent(
pointer: 5,
position: Offset(10.0, 10.0),
);
final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
..onStart = (_) {}; // Need a callback to compete
final List<String> recognized = <String>[];
tap.onTapDown = (_) {
recognized.add('down');
};
tap.onTapUp = (_) {
recognized.add('up');
};
tap.onTap = () {
recognized.add('tap');
};
tap.onTapCancel = () {
recognized.add('cancel');
};
tap.addPointer(down);
drag.addPointer(down);
tester.closeArena(5);
tester.route(down);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['down']);
tester.route(cancel);
expect(recognized, <String>['down', 'cancel']);
tap.dispose();
drag.dispose();
});
testGesture('losing tap gesture recognizer does not send onTapCancel', (GestureTester tester) { testGesture('losing tap gesture recognizer does not send onTapCancel', (GestureTester tester) {
final TapGestureRecognizer tap = TapGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
...@@ -467,4 +521,282 @@ void main() { ...@@ -467,4 +521,282 @@ void main() {
tap.dispose(); tap.dispose();
drag.dispose(); drag.dispose();
}); });
group('Enforce consistent-button restriction:', () {
// Change buttons during down-up sequence 1
const PointerMoveEvent move1lr = PointerMoveEvent(
pointer: 1,
position: Offset(10.0, 10.0),
buttons: kPrimaryMouseButton | kSecondaryMouseButton,
);
const PointerMoveEvent move1r = PointerMoveEvent(
pointer: 1,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
final List<String> recognized = <String>[];
TapGestureRecognizer tap;
setUp(() {
tap = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('down');
}
..onTapUp = (TapUpDetails details) {
recognized.add('up');
}
..onTapCancel = () {
recognized.add('cancel');
};
});
tearDown(() {
tap.dispose();
recognized.clear();
});
testGesture('changing buttons before TapDown should cancel gesture without sending cancel', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(1);
expect(recognized, <String>[]);
tester.route(move1lr);
expect(recognized, <String>[]);
tester.route(move1r);
expect(recognized, <String>[]);
tester.route(up1);
expect(recognized, <String>[]);
tap.dispose();
});
testGesture('changing buttons before TapDown should not prevent the next tap', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(1);
tester.route(move1lr);
tester.route(move1r);
tester.route(up1);
expect(recognized, <String>[]);
tap.addPointer(down2);
tester.closeArena(2);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(up2);
expect(recognized, <String>['down', 'up']);
tap.dispose();
});
testGesture('changing buttons after TapDown should cancel gesture and send cancel', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(1);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['down']);
tester.route(move1lr);
expect(recognized, <String>['down', 'cancel']);
tester.route(move1r);
expect(recognized, <String>['down', 'cancel']);
tester.route(up1);
expect(recognized, <String>['down', 'cancel']);
tap.dispose();
});
testGesture('changing buttons after TapDown should not prevent the next tap', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(1);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(move1lr);
tester.route(move1r);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(recognized, <String>['down', 'cancel']);
tap.addPointer(down2);
tester.closeArena(2);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(recognized, <String>['down', 'cancel', 'down', 'up']);
tap.dispose();
});
});
group('Recognizers listening on different buttons do not form competition:', () {
// If a tap gesture has no competitors, a pointer down event triggers
// onTapDown immediately; if there are competitors, onTapDown is triggered
// after a timeout. The following tests make sure that tap recognizers
// listening on different buttons do not form competition.
final List<String> recognized = <String>[];
TapGestureRecognizer primary;
TapGestureRecognizer primary2;
TapGestureRecognizer secondary;
setUp(() {
primary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('primaryDown');
}
..onTapUp = (TapUpDetails details) {
recognized.add('primaryUp');
}
..onTapCancel = () {
recognized.add('primaryCancel');
};
primary2 = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('primary2Down');
}
..onTapUp = (TapUpDetails details) {
recognized.add('primary2Up');
}
..onTapCancel = () {
recognized.add('primary2Cancel');
};
secondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('secondaryDown');
}
..onSecondaryTapUp = (TapUpDetails details) {
recognized.add('secondaryUp');
}
..onSecondaryTapCancel = () {
recognized.add('secondaryCancel');
};
});
tearDown(() {
recognized.clear();
primary.dispose();
primary2.dispose();
secondary.dispose();
});
testGesture('A primary tap recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
primary.addPointer(down1);
secondary.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
expect(recognized, <String>['primaryDown']);
recognized.clear();
tester.route(up1);
expect(recognized, <String>['primaryUp']);
});
testGesture('A primary tap recognizer forms competion with another primary tap recognizer', (GestureTester tester) {
primary.addPointer(down1);
primary2.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, <String>['primaryDown', 'primary2Down']);
});
});
group('Gestures of different buttons trigger correct callbacks:', () {
final List<String> recognized = <String>[];
TapGestureRecognizer tap;
const PointerCancelEvent cancel1 = PointerCancelEvent(
pointer: 1,
);
const PointerCancelEvent cancel5 = PointerCancelEvent(
pointer: 5,
);
setUp(() {
tap = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('primaryDown');
}
..onTap = () {
recognized.add('primary');
}
..onTapUp = (TapUpDetails details) {
recognized.add('primaryUp');
}
..onTapCancel = () {
recognized.add('primaryCancel');
}
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('secondaryDown');
}
..onSecondaryTapUp = (TapUpDetails details) {
recognized.add('secondaryUp');
}
..onSecondaryTapCancel = () {
recognized.add('secondaryCancel');
};
});
tearDown(() {
recognized.clear();
tap.dispose();
});
testGesture('A primary tap should trigger primary callbacks', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(down1.pointer);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, <String>['primaryDown']);
recognized.clear();
tester.route(up1);
expect(recognized, <String>['primaryUp', 'primary']);
GestureBinding.instance.gestureArena.sweep(down1.pointer);
});
testGesture('A primary tap cancel trigger primary callbacks', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(down1.pointer);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, <String>['primaryDown']);
recognized.clear();
tester.route(cancel1);
expect(recognized, <String>['primaryCancel']);
GestureBinding.instance.gestureArena.sweep(down1.pointer);
});
testGesture('A secondary tap should trigger secondary callbacks', (GestureTester tester) {
tap.addPointer(down5);
tester.closeArena(down5.pointer);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, <String>['secondaryDown']);
recognized.clear();
tester.route(up5);
GestureBinding.instance.gestureArena.sweep(down5.pointer);
expect(recognized, <String>['secondaryUp']);
});
testGesture('A secondary tap cancel should trigger secondary callbacks', (GestureTester tester) {
tap.addPointer(down5);
tester.closeArena(down5.pointer);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, <String>['secondaryDown']);
recognized.clear();
tester.route(cancel5);
GestureBinding.instance.gestureArena.sweep(down5.pointer);
expect(recognized, <String>['secondaryCancel']);
});
});
} }
...@@ -592,7 +592,10 @@ void main() { ...@@ -592,7 +592,10 @@ void main() {
viewType: 'webview', viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>( Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(), () {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
), ),
}, },
layoutDirection: TextDirection.ltr, layoutDirection: TextDirection.ltr,
...@@ -1263,7 +1266,10 @@ void main() { ...@@ -1263,7 +1266,10 @@ void main() {
viewType: 'webview', viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>( Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(), () {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
), ),
}, },
layoutDirection: TextDirection.ltr, layoutDirection: TextDirection.ltr,
......
...@@ -22,9 +22,14 @@ class TestPointer { ...@@ -22,9 +22,14 @@ class TestPointer {
/// ///
/// Multiple [TestPointer]s created with the same pointer identifier will /// Multiple [TestPointer]s created with the same pointer identifier will
/// interfere with each other if they are used in parallel. /// interfere with each other if they are used in parallel.
TestPointer([this.pointer = 1, this.kind = PointerDeviceKind.touch]) TestPointer([
this.pointer = 1,
this.kind = PointerDeviceKind.touch,
this.buttons = kPrimaryButton,
])
: assert(kind != null), : assert(kind != null),
assert(pointer != null); assert(pointer != null),
assert(buttons != null);
/// The pointer identifier used for events generated by this object. /// The pointer identifier used for events generated by this object.
/// ///
...@@ -35,6 +40,10 @@ class TestPointer { ...@@ -35,6 +40,10 @@ class TestPointer {
/// [PointerDeviceKind.touch]. /// [PointerDeviceKind.touch].
final PointerDeviceKind kind; final PointerDeviceKind kind;
/// The kind of buttons to simulate on Down and Move events. Defaults to
/// [kPrimaryButton].
int buttons;
/// Whether the pointer simulated by this object is currently down. /// Whether the pointer simulated by this object is currently down.
/// ///
/// A pointer is released (goes up) by calling [up] or [cancel]. /// A pointer is released (goes up) by calling [up] or [cancel].
...@@ -81,6 +90,7 @@ class TestPointer { ...@@ -81,6 +90,7 @@ class TestPointer {
kind: kind, kind: kind,
pointer: pointer, pointer: pointer,
position: location, position: location,
buttons: buttons,
); );
} }
...@@ -105,6 +115,7 @@ class TestPointer { ...@@ -105,6 +115,7 @@ class TestPointer {
pointer: pointer, pointer: pointer,
position: newLocation, position: newLocation,
delta: delta, delta: delta,
buttons: buttons,
); );
} }
......
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