Unverified Commit fea2c7d6 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Redo: Add buttons to gestures (#31819)

* Revert "Revert "Add buttons to gestures (#30339)" (#31801)"

This reverts commit 8fd7fa49.

* Synthesise kPrimaryButton for unknown devices

* Change TestPointer to a better API
parent 71a89a46
......@@ -47,12 +47,16 @@ class _PointerState {
// https://github.com/flutter/flutter/issues/30454
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
switch (kind) {
case PointerDeviceKind.mouse:
return buttons;
case PointerDeviceKind.touch:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
return buttons | kPrimaryButton;
default:
return buttons;
// We have no information about the device but we know we never want
// buttons to be 0 when the pointer is down.
return buttons == 0 ? kPrimaryButton : buttons;
}
}
......
......@@ -8,22 +8,35 @@ import 'package:flutter/foundation.dart';
export 'dart:ui' show Offset, PointerDeviceKind;
/// The bit of [PointerEvent.buttons] that corresponds to the "primary
/// action" on any device.
/// The bit of [PointerEvent.buttons] that corresponds to a cross-device
/// behavior of "primary operation".
///
/// More specifially,
/// More specifially, it includes:
///
/// * For touch screen, it's when the pointer contacts the screen.
/// * For stylus and inverted stylus, it's when the pen contacts the screen.
/// * For mouse, it's when the primary button is pressed.
/// * [kTouchContact]: The pointer contacts the touch screen.
/// * [kStylusContact]: The stylus contacts the screen.
/// * [kPrimaryMouseButton]: The primary mouse button.
///
/// See also:
///
/// * [kTouchContact]: an alias of this constant when used by touch screen.
/// * [kStylusContact]: an alias of this constant when used by stylus.
/// * [kPrimaryMouseButton]: an alias of this constant when used by mouse.
/// * [kSecondaryButton], which describes a cross-device behavior of
/// "secondary operation".
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 primary mouse button is typically the left button on the top of the
......@@ -31,28 +44,40 @@ const int kPrimaryButton = 0x01;
///
/// 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;
/// 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
/// 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
/// contacting the screen.
///
/// 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;
/// 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
/// 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.
///
......@@ -84,7 +109,8 @@ const int kForwardMouseButton = 0x10;
///
/// 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;
/// The bit of [PointerEvent.buttons] that corresponds to the nth mouse button.
......@@ -104,6 +130,47 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn
/// for some stylus buttons.
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.
///
/// Pointer events operate in the coordinate space of the screen, scaled to
......
......@@ -109,6 +109,10 @@ class LongPressEndDetails {
/// until it's recognized. Once the gesture is accepted, the finger can be
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
/// [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 {
/// Creates a long-press gesture recognizer.
///
......@@ -133,89 +137,159 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
);
bool _longPressAccepted = false;
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:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has data for the
/// press location.
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:
///
/// * [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;
/// 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;
/// 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:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has data for the up
/// gesture location.
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:
///
/// * [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;
@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
void didExceedDeadline() {
// Exceeding the deadline puts the gesture in the accepted state.
resolve(GestureDisposition.accepted);
_longPressAccepted = true;
super.acceptGesture(primaryPointer);
if (onLongPress != null) {
invokeCallback<void>('onLongPress', onLongPress);
}
if (onLongPressStart != null) {
invokeCallback<void>('onLongPressStart', () {
onLongPressStart(LongPressStartDetails(
globalPosition: _longPressOrigin,
));
});
}
_checkLongPressStart();
}
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
if (_longPressAccepted == true) {
if (onLongPressUp != null) {
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
if (onLongPressEnd != null) {
invokeCallback<void>('onLongPressEnd', () {
onLongPressEnd(LongPressEndDetails(
globalPosition: event.position,
));
});
}
_longPressAccepted = false;
_checkLongPressEnd(event);
} else {
// Pointer is lifted before timeout.
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.
_longPressAccepted = false;
_longPressOrigin = event.position;
} else if (event is PointerMoveEvent && _longPressAccepted && onLongPressMoveUpdate != null) {
invokeCallback<void>('onLongPressMoveUpdate', () {
onLongPressMoveUpdate(LongPressMoveUpdateDetails(
_initialButtons = event.buttons;
} else if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
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
......
......@@ -43,6 +43,10 @@ typedef GestureDragCancelCallback = void Function();
/// consider using one of its subclasses to recognize specific types for drag
/// 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:
///
/// * [HorizontalDragGestureRecognizer], for left and right drags.
......@@ -84,13 +88,20 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// at (510.0, 500.0).
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`
/// 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;
/// 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`
/// argument, which is a [DragStartDetails] object.
......@@ -99,23 +110,43 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// called on the initial touch down, if set to [DragStartBehavior.down] or
/// when the drag gesture is first detected, if set to
/// [DragStartBehavior.start].
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragStartDetails], which is passed as an argument to this callback.
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 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;
/// A pointer that was previously in contact with the screen and moving is no
/// longer in contact with the screen and was moving at a specific velocity
/// when it stopped contacting the screen.
/// A pointer that was previously in contact with the screen with a primary
/// button and moving is no longer in contact with the screen and was moving
/// at a specific velocity when it stopped contacting the screen.
///
/// The velocity is provided in the callback's `details` argument, which is a
/// [DragEndDetails] object.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [DragEndDetails], which is passed as an argument to this callback.
GestureDragEndCallback onEnd;
/// The pointer that previously triggered [onDown] did not complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
GestureDragCancelCallback onCancel;
/// The minimum distance an input pointer drag must have moved to
......@@ -141,6 +172,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset _initialPosition;
Offset _pendingDragOffset;
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);
Offset _getDeltaForDetails(Offset delta);
......@@ -149,6 +183,30 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
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
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
......@@ -156,10 +214,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == _DragState.ready) {
_state = _DragState.possible;
_initialPosition = event.position;
_initialButtons = event.buttons;
_pendingDragOffset = Offset.zero;
_lastPendingEventTimestamp = event.timeStamp;
if (onDown != null)
invokeCallback<void>('onDown', () => onDown(DragDownDetails(globalPosition: _initialPosition)));
_checkDown();
} else if (_state == _DragState.accepted) {
resolve(GestureDisposition.accepted);
}
......@@ -176,16 +234,19 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(event.pointer);
return;
}
final Offset delta = event.delta;
if (_state == _DragState.accepted) {
if (onUpdate != null) {
invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
_checkUpdate(
sourceTimeStamp: event.timeStamp,
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryValueFromOffset(delta),
globalPosition: event.position,
)));
}
);
} else {
_pendingDragOffset += delta;
_lastPendingEventTimestamp = event.timeStamp;
......@@ -214,19 +275,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
_pendingDragOffset = Offset.zero;
_lastPendingEventTimestamp = null;
if (onStart != null) {
invokeCallback<void>('onStart', () => onStart(DragStartDetails(
sourceTimeStamp: timestamp,
globalPosition: _initialPosition,
)));
}
if (updateDelta != Offset.zero && onUpdate != null) {
invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
_checkStart(timestamp);
if (updateDelta != Offset.zero) {
_checkUpdate(
sourceTimeStamp: timestamp,
delta: updateDelta,
primaryDelta: _getPrimaryValueFromOffset(updateDelta),
globalPosition: _initialPosition + updateDelta, // Only adds delta for down behaviour
)));
);
}
}
}
......@@ -238,41 +294,101 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void didStopTrackingLastPointer(int pointer) {
if (_state == _DragState.possible) {
assert(_state != _DragState.ready);
switch(_state) {
case _DragState.ready:
break;
case _DragState.possible:
resolve(GestureDisposition.rejected);
_state = _DragState.ready;
if (onCancel != null)
invokeCallback<void>('onCancel', onCancel);
return;
_checkCancel();
break;
case _DragState.accepted:
_checkEnd(pointer);
break;
}
final bool wasAccepted = _state == _DragState.accepted;
_velocityTrackers.clear();
_initialButtons = null;
_state = _DragState.ready;
if (wasAccepted && onEnd != null) {
}
void _checkDown() {
assert(_initialButtons == kPrimaryButton);
final DragDownDetails details = DragDownDetails(
globalPosition: _initialPosition,
);
if (onDown != null)
invokeCallback<void>('onDown', () => onDown(details));
}
void _checkStart(Duration timestamp) {
assert(_initialButtons == kPrimaryButton);
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
globalPosition: _initialPosition,
);
if (onStart != null)
invokeCallback<void>('onStart', () => onStart(details));
}
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);
invokeCallback<void>('onEnd', () => onEnd(DragEndDetails(
details = DragEndDetails(
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
)), debugReport: () {
);
debugReport = () {
return '$estimate; fling at $velocity.';
});
};
} else {
invokeCallback<void>('onEnd', () => onEnd(DragEndDetails(
details = DragEndDetails(
velocity: Velocity.zero,
primaryVelocity: 0.0,
)), debugReport: () {
);
debugReport = () {
if (estimate == null)
return 'Could not estimate velocity.';
return '$estimate; judged to not be a fling.';
});
};
}
invokeCallback<void>('onEnd', () => onEnd(details), debugReport: debugReport);
}
_velocityTrackers.clear();
void _checkCancel() {
assert(_initialButtons == kPrimaryButton);
if (onCancel != null)
invokeCallback<void>('onCancel', onCancel);
}
@override
......
......@@ -16,6 +16,10 @@ import 'tap.dart';
/// Signature for callback when the user has tapped the screen at the same
/// location twice in quick succession.
///
/// See also:
///
/// * [GestureDetector.onDoubleTap], which matches this signature.
typedef GestureDoubleTapCallback = void Function();
/// Signature used by [MultiTapGestureRecognizer] for when a pointer that might
......@@ -60,13 +64,16 @@ class _TapTracker {
@required Duration doubleTapMinTime,
}) : assert(doubleTapMinTime != null),
assert(event != null),
assert(event.buttons != null),
pointer = event.pointer,
_initialPosition = event.position,
initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer;
final GestureArenaEntry entry;
final Offset _initialPosition;
final int initialButtons;
final _CountdownZoned _doubleTapMinTimeCountdown;
bool _isTrackingPointer = false;
......@@ -93,10 +100,18 @@ class _TapTracker {
bool hasElapsedMinTime() {
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
/// 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 {
/// Create a gesture recognizer for double taps.
///
......@@ -126,26 +141,53 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
// - The long timer between taps expires
// - The gesture arena decides we have been rejected wholesale
/// Called when the user has tapped the screen at the same location twice in
/// quick succession.
/// Called when the user has tapped the screen with a primary button at the
/// 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;
Timer _doubleTapTimer;
_TapTracker _firstTap;
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
void addAllowedPointer(PointerEvent event) {
if (_firstTap != null) {
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap.hasElapsedMinTime()) {
// Restart when the second tap is too close to the first.
} else if (!_firstTap.hasElapsedMinTime() || !_firstTap.hasSameButton(event)) {
// Restart when the second tap is too close to the first, or when buttons
// mismatch.
_reset();
return addAllowedPointer(event);
return _trackFirstTap(event);
}
}
_trackFirstTap(event);
}
void _trackFirstTap(PointerEvent event) {
_stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker(
event: event,
......@@ -235,8 +277,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
tracker.entry.resolve(GestureDisposition.accepted);
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
if (onDoubleTap != null)
invokeCallback<void>('onDoubleTap', onDoubleTap);
_checkUp(tracker.initialButtons);
_reset();
}
......@@ -260,6 +301,12 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
}
}
void _checkUp(int buttons) {
assert(buttons == kPrimaryButton);
if (onDoubleTap != null)
invokeCallback<void>('onDoubleTap', onDoubleTap);
}
@override
String get debugDescription => 'double tap';
}
......
......@@ -9,7 +9,7 @@ import 'constants.dart';
import 'events.dart';
import 'recognizer.dart';
/// Details for [GestureTapDownCallback], such as position.
/// Details for [GestureTapDownCallback], such as position
///
/// See also:
///
......@@ -45,8 +45,6 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details);
/// * [GestureDetector.onTapUp], which receives this information.
/// * [TapGestureRecognizer], which passes this information to one of its callbacks.
class TapUpDetails {
/// Creates details for a [GestureTapUpCallback].
///
/// The [globalPosition] argument must not be null.
TapUpDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);
......@@ -95,14 +93,10 @@ typedef GestureTapCancelCallback = void Function();
/// 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.
///
/// The lifecycle of events for a tap gesture is as follows:
///
/// * [onTapDown], which triggers after a short timeout ([deadline]) even if the
/// gesture has not won its arena yet.
/// * [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.
/// [TapGestureRecognizer] competes on pointer events of [kPrimaryButton] only
/// when it has at least one non-null `onTap*` callback, and events of
/// [kSecondaryButton] only when it has at least one non-null `onSecondaryTap*`
/// callback. If it has no callbacks, it is a no-op.
///
/// See also:
///
......@@ -112,22 +106,25 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a tap gesture recognizer.
TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
/// A pointer that might cause a tap has contacted the screen at a particular
/// location.
/// A pointer that might cause a tap of a primary button has contacted the
/// screen at a particular location.
///
/// This triggers before the gesture has won the arena, after a short timeout
/// ([deadline]).
/// 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, [onTapCancel] is called next.
/// Otherwise, [onTapUp] is called next.
///
/// 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.
GestureTapDownCallback onTapDown;
/// A pointer that will trigger a tap has stopped contacting the screen at a
/// particular location.
/// A pointer that will trigger a tap of a primary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers once the gesture has won the arena, immediately before
/// [onTap].
......@@ -136,11 +133,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
///
/// 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.
/// * [GestureDetector.onTapUp], which exposes this callback.
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
/// [onTapUp].
......@@ -149,6 +148,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onTapUp], which has the same timing but with details.
/// * [GestureDetector.onTap], which exposes this callback.
GestureTapCallback onTap;
......@@ -161,37 +162,121 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryTapCancel], a similar callback but for a secondary button.
/// * [GestureDetector.onTapCancel], which exposes this callback.
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 _wonArenaForPrimaryPointer = false;
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
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_finalPosition = event.position;
if (_wonArenaForPrimaryPointer) {
resolve(GestureDisposition.accepted);
_checkUp();
}
} else if (event is PointerCancelEvent) {
if (_sentTapDown && onTapCancel != null) {
invokeCallback<void>('onTapCancel', onTapCancel);
resolve(GestureDisposition.rejected);
if (_sentTapDown) {
_checkCancel('');
}
_reset();
} else if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
}
}
@override
void resolve(GestureDisposition disposition) {
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
// This can happen if the superclass decides the primary pointer
// exceeded the touch slop, or if the recognizer is disposed.
// This can happen if the gesture has been canceled. For example, when
// the pointer has exceeded the touch slop, the buttons have been changed,
// or if the recognizer is disposed.
assert(_sentTapDown);
if (onTapCancel != null)
invokeCallback<void>('spontaneous onTapCancel', onTapCancel);
_checkCancel('spontaneous ');
_reset();
}
super.resolve(disposition);
......@@ -218,34 +303,78 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
if (pointer == primaryPointer) {
// Another gesture won the arena.
assert(state != GestureRecognizerState.possible);
if (_sentTapDown && onTapCancel != null)
invokeCallback<void>('forced onTapCancel', onTapCancel);
if (_sentTapDown)
_checkCancel('forced ');
_reset();
}
}
void _checkDown() {
if (!_sentTapDown) {
if (_sentTapDown) {
return;
}
final TapDownDetails details = TapDownDetails(
globalPosition: initialPosition,
);
switch (_initialButtons) {
case kPrimaryButton:
if (onTapDown != null)
invokeCallback<void>('onTapDown', () { onTapDown(TapDownDetails(globalPosition: initialPosition)); });
_sentTapDown = true;
invokeCallback<void>('onTapDown', () => onTapDown(details));
break;
case kSecondaryButton:
if (onSecondaryTapDown != null)
invokeCallback<void>('onSecondaryTapDown',
() => onSecondaryTapDown(details));
break;
default:
}
_sentTapDown = true;
}
void _checkUp() {
if (_finalPosition != null) {
if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
return;
}
final TapUpDetails details = TapUpDetails(
globalPosition: _finalPosition,
);
switch (_initialButtons) {
case kPrimaryButton:
if (onTapUp != null)
invokeCallback<void>('onTapUp', () { onTapUp(TapUpDetails(globalPosition: _finalPosition)); });
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:
}
}
void _reset() {
_sentTapDown = false;
_wonArenaForPrimaryPointer = false;
_finalPosition = null;
_initialButtons = null;
}
@override
......@@ -257,5 +386,6 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null));
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 {
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.onSecondaryTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onLongPressStart,
......@@ -229,28 +232,37 @@ class GestureDetector extends StatelessWidget {
/// {@macro flutter.widgets.child}
final Widget child;
/// A pointer that might cause a tap has contacted the screen at a particular
/// location.
/// A pointer that might cause a tap with a primary button has contacted the
/// 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, [onTapUp] will be called,
/// otherwise [onTapCancel] will be called.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapDownCallback onTapDown;
/// A pointer that will trigger a tap has stopped contacting the screen at a
/// particular location.
/// A pointer that will trigger a tap with a primary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers immediately before [onTap] in the case of the tap gesture
/// winning. If the tap gesture did not win, [onTapCancel] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
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,
/// [onTapCancel] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onTapUp], which is called at the same time but includes details
/// regarding the pointer position.
final GestureTapCallback onTap;
......@@ -260,104 +272,222 @@ class GestureDetector extends StatelessWidget {
///
/// This is called after [onTapDown], and instead of [onTapUp] and [onTap], if
/// the tap gesture did not win.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback onTapCancel;
/// The user has tapped the screen at the same location twice in quick
/// succession.
/// A pointer that might cause a tap with a secondary button has contacted the
/// 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;
/// 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
/// same location for a long period of time.
///
/// See also:
///
/// * [onLongPressStart], which has the same timing but has data for the
/// press location.
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has gesture details.
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
/// same location for a long period of time.
///
/// 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;
/// 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;
/// 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:
///
/// * [onLongPressEnd], which has the same timing but has data for the up
/// gesture location.
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has gesture details.
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:
///
/// * [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;
/// 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;
/// 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;
/// A pointer that is in contact with the screen and moving vertically has
/// moved in the vertical direction.
/// A pointer that is in contact with the screen with a primary button and
/// moving vertically has moved in the vertical direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback onVerticalDragUpdate;
/// A pointer that was previously in contact with the screen and moving
/// vertically is no longer in contact with the screen and was moving at a
/// specific velocity when it stopped contacting the screen.
/// A pointer that was previously in contact with the screen with a primary
/// button and moving vertically is no longer in contact with the screen and
/// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onVerticalDragEnd;
/// The pointer that previously triggered [onVerticalDragDown] did not
/// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
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;
/// 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;
/// A pointer that is in contact with the screen and moving horizontally has
/// moved in the horizontal direction.
/// A pointer that is in contact with the screen with a primary button and
/// moving horizontally has moved in the horizontal direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback onHorizontalDragUpdate;
/// A pointer that was previously in contact with the screen and moving
/// horizontally is no longer in contact with the screen and was moving at a
/// specific velocity when it stopped contacting the screen.
/// A pointer that was previously in contact with the screen with a primary
/// button and moving horizontally is no longer in contact with the screen and
/// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onHorizontalDragEnd;
/// The pointer that previously triggered [onHorizontalDragDown] did not
/// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
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;
/// 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;
/// 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;
/// A pointer that was previously in contact with the screen and moving
/// is no longer in contact with the screen and was moving at a specific
/// velocity when it stopped contacting the screen.
/// A pointer that was previously in contact with the screen with a primary
/// button and moving is no longer in contact with the screen and was moving
/// at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback onPanEnd;
/// The pointer that previously triggered [onPanDown] did not complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback onPanCancel;
/// The pointers in contact with the screen have established a focal point and
......@@ -440,7 +570,15 @@ class GestureDetector extends StatelessWidget {
Widget build(BuildContext context) {
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>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
......@@ -448,7 +586,10 @@ class GestureDetector extends StatelessWidget {
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel;
..onTapCancel = onTapCancel
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel;
},
);
}
......@@ -797,8 +938,14 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
void _handleSemanticsLongPress() {
final LongPressGestureRecognizer recognizer = _recognizers[LongPressGestureRecognizer];
assert(recognizer != null);
if (recognizer.onLongPressStart != null)
recognizer.onLongPressStart(const LongPressStartDetails());
if (recognizer.onLongPress != null)
recognizer.onLongPress();
if (recognizer.onLongPressEnd != null)
recognizer.onLongPressEnd(const LongPressEndDetails());
if (recognizer.onLongPressUp != null)
recognizer.onLongPressUp();
}
void _handleSemanticsHorizontalDragUpdate(DragUpdateDetails updateDetails) {
......
......@@ -147,7 +147,8 @@ void main() {
});
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)'));
const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
tap.addPointer(event);
......
......@@ -75,7 +75,7 @@ void main() {
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(
pointer: 5,
position: Offset(10.0, 10.0),
......@@ -86,6 +86,18 @@ void main() {
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) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
......@@ -606,4 +618,183 @@ void main() {
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() {
testGesture('Should recognize pan', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer();
final TapGestureRecognizer tap = TapGestureRecognizer();
final TapGestureRecognizer tap = TapGestureRecognizer()..onTap = () {};
bool didStartPan = false;
pan.onStart = (_) {
......@@ -81,7 +81,8 @@ void main() {
testGesture('Should report most recent point to onStart by default', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..onStart = (_) {};
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -103,9 +104,9 @@ void main() {
});
testGesture('Should report most recent point to onStart with a start configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..onStart = (_) {};
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -218,9 +219,11 @@ void main() {
// TODO(jslavitz): Revert these tests.
testGesture('Should report initial down point to onStart with a down configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down;
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down
..onStart = (_) {};
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -596,4 +599,248 @@ void main() {
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']);
// Move out of slop so make sure button changes takes priority over slops
tester.route(pointer.move(const Offset(30.0, 30.0), buttons: kSecondaryButton));
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);
tester.route(pointer.move(const Offset(10.0, 10.0), buttons: kSecondaryButton));
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']);
tester.route(pointer.move(const Offset(30.0, 30.0), buttons: kSecondaryButton));
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)));
tester.route(pointer.move(const Offset(30.0, 31.0), buttons: kSecondaryButton));
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() {
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:', () {
// Some parameters are intentionally set to a non-trivial value.
......
......@@ -318,7 +318,38 @@ void main() {
}
});
test('Should not synthesise kPrimaryButton for certain devices', () {
test('Should synthesise kPrimaryButton for unknown devices', () {
final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
const PointerDeviceKind kind = PointerDeviceKind.unknown;
final ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
ui.PointerData(change: ui.PointerChange.down, kind: kind, physicalX: location.dx, physicalY: location.dy),
ui.PointerData(change: ui.PointerChange.move, kind: kind, physicalX: location.dx, physicalY: location.dy),
ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
]
);
final List<PointerEvent> events = PointerEventConverter.expand(
packet.data, ui.window.devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0].runtimeType, equals(PointerAddedEvent));
expect(events[0].buttons, equals(0));
expect(events[1].runtimeType, equals(PointerHoverEvent));
expect(events[1].buttons, equals(0));
expect(events[2].runtimeType, equals(PointerDownEvent));
expect(events[2].buttons, equals(kPrimaryButton));
expect(events[3].runtimeType, equals(PointerMoveEvent));
expect(events[3].buttons, equals(kPrimaryButton));
expect(events[4].runtimeType, equals(PointerUpEvent));
expect(events[4].buttons, equals(0));
PointerEventConverter.clearPointers();
});
test('Should not synthesise kPrimaryButton for mouse', () {
final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
for (PointerDeviceKind kind in <PointerDeviceKind>[
PointerDeviceKind.mouse,
......
......@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import '../flutter_test_alternative.dart';
import 'gesture_tester.dart';
// Down/move/up pair 1: normal tap sequence
const PointerDownEvent down = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
......@@ -22,6 +23,29 @@ const PointerMoveEvent move = PointerMoveEvent(
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() {
setUp(ensureGestureBinding);
......@@ -187,6 +211,48 @@ void main() {
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', () {
......@@ -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) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse);
......@@ -318,4 +485,107 @@ void main() {
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() {
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) {
final TapGestureRecognizer tap = TapGestureRecognizer();
......@@ -439,6 +451,48 @@ void main() {
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) {
final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
......@@ -467,4 +521,282 @@ void main() {
tap.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() {
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
() {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
),
},
layoutDirection: TextDirection.ltr,
......@@ -1263,7 +1266,10 @@ void main() {
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
() {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
),
},
layoutDirection: TextDirection.ltr,
......
......@@ -22,9 +22,15 @@ class TestPointer {
///
/// Multiple [TestPointer]s created with the same pointer identifier will
/// 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,
int buttons = kPrimaryButton,
])
: assert(kind != null),
assert(pointer != null);
assert(pointer != null),
assert(buttons != null),
_buttons = buttons;
/// The pointer identifier used for events generated by this object.
///
......@@ -35,6 +41,11 @@ class TestPointer {
/// [PointerDeviceKind.touch].
final PointerDeviceKind kind;
/// The kind of buttons to simulate on Down and Move events. Defaults to
/// [kPrimaryButton].
int get buttons => _buttons;
int _buttons;
/// Whether the pointer simulated by this object is currently down.
///
/// A pointer is released (goes up) by calling [up] or [cancel].
......@@ -51,8 +62,14 @@ class TestPointer {
/// If a custom event is created outside of this class, this function is used
/// to set the [isDown].
bool setDownInfo(PointerEvent event, Offset newLocation) {
bool setDownInfo(
PointerEvent event,
Offset newLocation, {
int buttons,
}) {
_location = newLocation;
if (buttons != null)
_buttons = buttons;
switch (event.runtimeType) {
case PointerDownEvent:
assert(!isDown);
......@@ -72,15 +89,25 @@ class TestPointer {
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerDownEvent down(Offset newLocation, {Duration timeStamp = Duration.zero}) {
///
/// By default, the set of buttons in the last down or move event is used.
/// You can give a specific set of buttons by passing the `buttons` argument.
PointerDownEvent down(
Offset newLocation, {
Duration timeStamp = Duration.zero,
int buttons,
}) {
assert(!isDown);
_isDown = true;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerDownEvent(
timeStamp: timeStamp,
kind: kind,
pointer: pointer,
position: location,
buttons: _buttons,
);
}
......@@ -91,7 +118,14 @@ class TestPointer {
///
/// [isDown] must be true when this is called, since move events can only
/// be generated when the pointer is down.
PointerMoveEvent move(Offset newLocation, {Duration timeStamp = Duration.zero}) {
///
/// By default, the set of buttons in the last down or move event is used.
/// You can give a specific set of buttons by passing the `buttons` argument.
PointerMoveEvent move(
Offset newLocation, {
Duration timeStamp = Duration.zero,
int buttons,
}) {
assert(
isDown,
'Move events can only be generated when the pointer is down. To '
......@@ -99,12 +133,15 @@ class TestPointer {
'up, use hover() instead.');
final Offset delta = newLocation - location;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerMoveEvent(
timeStamp: timeStamp,
kind: kind,
pointer: pointer,
position: newLocation,
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