mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Redo: Add buttons to gestures (#31819)
* Revert "Revert "Add buttons to gestures (#30339)" (#31801)"
This reverts commit 8fd7fa492a
.
* Synthesise kPrimaryButton for unknown devices
* Change TestPointer to a better API
This commit is contained in:
parent
71a89a469a
commit
fea2c7d671
@ -47,12 +47,16 @@ class _PointerState {
|
|||||||
// https://github.com/flutter/flutter/issues/30454
|
// https://github.com/flutter/flutter/issues/30454
|
||||||
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
|
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
|
case PointerDeviceKind.mouse:
|
||||||
|
return buttons;
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
return buttons | kPrimaryButton;
|
return buttons | kPrimaryButton;
|
||||||
default:
|
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;
|
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;
|
|||||||
///
|
///
|
||||||
/// 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;
|
|||||||
///
|
///
|
||||||
/// 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
|
|||||||
/// 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 {
|
|||||||
/// 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,91 +137,161 @@ 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.
|
||||||
GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
|
|
||||||
|
|
||||||
/// Called when the pointer stops contacting the screen after the long-press.
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// 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 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
|
/// * [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
|
||||||
void acceptGesture(int pointer) {
|
void acceptGesture(int pointer) {
|
||||||
// Winning the arena isn't important here since it may happen from a sweep.
|
// Winning the arena isn't important here since it may happen from a sweep.
|
||||||
|
@ -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 {
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
_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 {
|
|||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
final bool wasAccepted = _state == _DragState.accepted;
|
|
||||||
_state = _DragState.ready;
|
|
||||||
if (wasAccepted && onEnd != null) {
|
|
||||||
final VelocityTracker tracker = _velocityTrackers[pointer];
|
|
||||||
assert(tracker != null);
|
|
||||||
|
|
||||||
final VelocityEstimate estimate = tracker.getVelocityEstimate();
|
case _DragState.possible:
|
||||||
if (estimate != null && _isFlingGesture(estimate)) {
|
resolve(GestureDisposition.rejected);
|
||||||
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
|
_checkCancel();
|
||||||
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
|
break;
|
||||||
invokeCallback<void>('onEnd', () => onEnd(DragEndDetails(
|
|
||||||
velocity: velocity,
|
case _DragState.accepted:
|
||||||
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
|
_checkEnd(pointer);
|
||||||
)), debugReport: () {
|
break;
|
||||||
return '$estimate; fling at $velocity.';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
invokeCallback<void>('onEnd', () => onEnd(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();
|
_velocityTrackers.clear();
|
||||||
|
_initialButtons = null;
|
||||||
|
_state = _DragState.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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.';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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';
|
|||||||
|
|
||||||
/// 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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
// - 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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
|||||||
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);
|
|||||||
/// * [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();
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
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 {
|
|||||||
_sentTapDown = false;
|
_sentTapDown = false;
|
||||||
_wonArenaForPrimaryPointer = false;
|
_wonArenaForPrimaryPointer = false;
|
||||||
_finalPosition = null;
|
_finalPosition = null;
|
||||||
|
_initialButtons = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
/// {@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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
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 {
|
|||||||
..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> {
|
|||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
// 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,248 @@ 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']);
|
||||||
|
// 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);
|
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.
|
||||||
|
|
||||||
|
@ -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;
|
final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
|
||||||
for (PointerDeviceKind kind in <PointerDeviceKind>[
|
for (PointerDeviceKind kind in <PointerDeviceKind>[
|
||||||
PointerDeviceKind.mouse,
|
PointerDeviceKind.mouse,
|
||||||
|
@ -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(
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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,15 @@ 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,
|
||||||
|
int buttons = kPrimaryButton,
|
||||||
|
])
|
||||||
: assert(kind != null),
|
: assert(kind != null),
|
||||||
assert(pointer != null);
|
assert(pointer != null),
|
||||||
|
assert(buttons != null),
|
||||||
|
_buttons = buttons;
|
||||||
|
|
||||||
/// The pointer identifier used for events generated by this object.
|
/// The pointer identifier used for events generated by this object.
|
||||||
///
|
///
|
||||||
@ -35,6 +41,11 @@ 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 get buttons => _buttons;
|
||||||
|
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].
|
||||||
@ -51,8 +62,14 @@ class TestPointer {
|
|||||||
|
|
||||||
/// If a custom event is created outside of this class, this function is used
|
/// If a custom event is created outside of this class, this function is used
|
||||||
/// to set the [isDown].
|
/// to set the [isDown].
|
||||||
bool setDownInfo(PointerEvent event, Offset newLocation) {
|
bool setDownInfo(
|
||||||
|
PointerEvent event,
|
||||||
|
Offset newLocation, {
|
||||||
|
int buttons,
|
||||||
|
}) {
|
||||||
_location = newLocation;
|
_location = newLocation;
|
||||||
|
if (buttons != null)
|
||||||
|
_buttons = buttons;
|
||||||
switch (event.runtimeType) {
|
switch (event.runtimeType) {
|
||||||
case PointerDownEvent:
|
case PointerDownEvent:
|
||||||
assert(!isDown);
|
assert(!isDown);
|
||||||
@ -72,15 +89,25 @@ class TestPointer {
|
|||||||
///
|
///
|
||||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||||
/// specific time stamp by passing the `timeStamp` argument.
|
/// 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);
|
assert(!isDown);
|
||||||
_isDown = true;
|
_isDown = true;
|
||||||
_location = newLocation;
|
_location = newLocation;
|
||||||
|
if (buttons != null)
|
||||||
|
_buttons = buttons;
|
||||||
return PointerDownEvent(
|
return PointerDownEvent(
|
||||||
timeStamp: timeStamp,
|
timeStamp: timeStamp,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
pointer: pointer,
|
pointer: pointer,
|
||||||
position: location,
|
position: location,
|
||||||
|
buttons: _buttons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +118,14 @@ class TestPointer {
|
|||||||
///
|
///
|
||||||
/// [isDown] must be true when this is called, since move events can only
|
/// [isDown] must be true when this is called, since move events can only
|
||||||
/// be generated when the pointer is down.
|
/// 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(
|
assert(
|
||||||
isDown,
|
isDown,
|
||||||
'Move events can only be generated when the pointer is down. To '
|
'Move events can only be generated when the pointer is down. To '
|
||||||
@ -99,12 +133,15 @@ class TestPointer {
|
|||||||
'up, use hover() instead.');
|
'up, use hover() instead.');
|
||||||
final Offset delta = newLocation - location;
|
final Offset delta = newLocation - location;
|
||||||
_location = newLocation;
|
_location = newLocation;
|
||||||
|
if (buttons != null)
|
||||||
|
_buttons = buttons;
|
||||||
return PointerMoveEvent(
|
return PointerMoveEvent(
|
||||||
timeStamp: timeStamp,
|
timeStamp: timeStamp,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
pointer: pointer,
|
pointer: pointer,
|
||||||
position: newLocation,
|
position: newLocation,
|
||||||
delta: delta,
|
delta: delta,
|
||||||
|
buttons: _buttons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user