Unverified Commit 60a1b2b9 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Revert "Redo: Add buttons to gestures (#31819)" (#31912)

This reverts commit fea2c7d6.
parent df4dc7cd
......@@ -47,16 +47,12 @@ class _PointerState {
// https://github.com/flutter/flutter/issues/30454
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
switch (kind) {
case PointerDeviceKind.mouse:
return buttons;
case PointerDeviceKind.touch:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
return buttons | kPrimaryButton;
default:
// 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;
return buttons;
}
}
......
......@@ -8,35 +8,22 @@ import 'package:flutter/foundation.dart';
export 'dart:ui' show Offset, PointerDeviceKind;
/// The bit of [PointerEvent.buttons] that corresponds to a cross-device
/// behavior of "primary operation".
/// The bit of [PointerEvent.buttons] that corresponds to the "primary
/// action" on any device.
///
/// More specifially, it includes:
/// More specifially,
///
/// * [kTouchContact]: The pointer contacts the touch screen.
/// * [kStylusContact]: The stylus contacts the screen.
/// * [kPrimaryMouseButton]: The primary mouse button.
/// * For touch screen, it's when the pointer contacts the screen.
/// * For stylus and inverted stylus, it's when the pen contacts the screen.
/// * For mouse, it's when the primary button is pressed.
///
/// See also:
///
/// * [kSecondaryButton], which describes a cross-device behavior of
/// "secondary operation".
/// * [kTouchContact]: an alias of this constant when used by touch screen.
/// * [kStylusContact]: an alias of this constant when used by stylus.
/// * [kPrimaryMouseButton]: an alias of this constant when used by mouse.
const int kPrimaryButton = 0x01;
/// The bit of [PointerEvent.buttons] that corresponds to a cross-device
/// behavior of "secondary operation".
///
/// It is equivalent to:
///
/// * [kPrimaryStylusButton]: The stylus contacts the screen.
/// * [kSecondaryMouseButton]: The primary mouse button.
///
/// See also:
///
/// * [kPrimaryButton], which describes a cross-device behavior of
/// "primary operation".
const int kSecondaryButton = 0x02;
/// The bit of [PointerEvent.buttons] that corresponds to the primary mouse button.
///
/// The primary mouse button is typically the left button on the top of the
......@@ -44,40 +31,28 @@ const int kSecondaryButton = 0x02;
///
/// See also:
///
/// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
/// * [kTouchContact]: an alias of this constant when used by touch screen.
const int kPrimaryMouseButton = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the secondary mouse button.
///
/// The secondary mouse button is typically the right button on the top of the
/// mouse but can be reconfigured to be a different physical button.
///
/// See also:
///
/// * [kSecondaryButton], which has the same value but describes its cross-device
/// concept.
const int kSecondaryMouseButton = kSecondaryButton;
const int kSecondaryMouseButton = 0x02;
/// The bit of [PointerEvent.buttons] that corresponds to when a stylus
/// contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
/// * [kPrimaryButton]: an alias of this constant for any device.
const int kStylusContact = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the primary stylus button.
///
/// The primary stylus button is typically the top of the stylus and near the
/// tip but can be reconfigured to be a different physical button.
///
/// See also:
///
/// * [kSecondaryButton], which has the same value but describes its cross-device
/// concept.
const int kPrimaryStylusButton = kSecondaryButton;
const int kPrimaryStylusButton = 0x02;
/// The bit of [PointerEvent.buttons] that corresponds to the middle mouse button.
///
......@@ -109,8 +84,7 @@ const int kForwardMouseButton = 0x10;
///
/// See also:
///
/// * [kPrimaryButton], which has the same value but describes its cross-device
/// concept.
/// * [kPrimaryButton]: an alias of this constant for any device.
const int kTouchContact = kPrimaryButton;
/// The bit of [PointerEvent.buttons] that corresponds to the nth mouse button.
......@@ -130,47 +104,6 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn
/// for some stylus buttons.
int nthStylusButton(int number) => (kPrimaryStylusButton << (number - 1)) & kMaxUnsignedSMI;
/// Returns the button of `buttons` with the smallest integer.
///
/// The `buttons` parameter is a bitfield where each set bit represents a button.
/// This function returns the set bit closest to the least significant bit.
///
/// It returns zero when `buttons` is zero.
///
/// Example:
///
/// ```dart
/// assert(rightmostButton(0x1) == 0x1);
/// assert(rightmostButton(0x11) == 0x1);
/// assert(rightmostButton(0) == 0);
/// ```
///
/// See also:
///
/// * [isSingleButton], which checks if a `buttons` contains exactly one button.
int smallestButton(int buttons) => buttons & (-buttons);
/// Returns whether `buttons` contains one and only one button.
///
/// The `buttons` parameter is a bitfield where each set bit represents a button.
/// This function returns whether there is only one set bit in the given integer.
///
/// It returns false when `buttons` is zero.
///
/// Example:
///
/// ```dart
/// assert(isSingleButton(0x1) == true);
/// assert(isSingleButton(0x11) == false);
/// assert(isSingleButton(0) == false);
/// ```
///
/// See also:
///
/// * [smallestButton], which returns the button in a `buttons` bitfield with
/// the smallest integer button.
bool isSingleButton(int buttons) => buttons != 0 && (smallestButton(buttons) == buttons);
/// Base class for touch, stylus, or mouse events.
///
/// Pointer events operate in the coordinate space of the screen, scaled to
......
......@@ -109,10 +109,6 @@ class LongPressEndDetails {
/// until it's recognized. Once the gesture is accepted, the finger can be
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
/// [postAcceptSlopTolerance] constructor argument is specified.
///
/// [LongPressGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a long-press gesture recognizer.
///
......@@ -137,159 +133,89 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
);
bool _longPressAccepted = false;
Offset _longPressOrigin;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
/// Called when a long press gesture by a primary button has been recognized.
/// Called when a long press gesture has been recognized.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has data for the
/// press location.
GestureLongPressCallback onLongPress;
/// Called when a long press gesture by a primary button has been recognized.
/// Callback for long press start with gesture location.
///
/// See also:
///
/// * [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.
/// * [onLongPress], which has the same timing but without the location data.
GestureLongPressStartCallback onLongPressStart;
/// Called when moving after the long press by a primary button is recognized.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressMoveUpdateDetails], which is passed as an argument to this
/// callback.
/// Callback for moving the gesture after the lang press is recognized.
GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
/// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
/// Called when the pointer stops contacting the screen after the long-press.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has data for the up
/// gesture location.
GestureLongPressUpCallback onLongPressUp;
/// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
/// Callback for long press end with gesture location.
///
/// See also:
///
/// * [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.
/// * [onLongPressUp], which has the same timing but without the location data.
GestureLongPressEndCallback onLongPressEnd;
@override
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {
case kPrimaryButton:
if (onLongPressStart == null &&
onLongPress == null &&
onLongPressMoveUpdate == null &&
onLongPressEnd == null &&
onLongPressUp == null)
return false;
break;
default:
return false;
}
return super.isPointerAllowed(event);
}
@override
void didExceedDeadline() {
// Exceeding the deadline puts the gesture in the accepted state.
resolve(GestureDisposition.accepted);
_longPressAccepted = true;
super.acceptGesture(primaryPointer);
_checkLongPressStart();
if (onLongPress != null) {
invokeCallback<void>('onLongPress', onLongPress);
}
if (onLongPressStart != null) {
invokeCallback<void>('onLongPressStart', () {
onLongPressStart(LongPressStartDetails(
globalPosition: _longPressOrigin,
));
});
}
}
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
if (_longPressAccepted == true) {
_checkLongPressEnd(event);
if (onLongPressUp != null) {
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
if (onLongPressEnd != null) {
invokeCallback<void>('onLongPressEnd', () {
onLongPressEnd(LongPressEndDetails(
globalPosition: event.position,
));
});
}
_longPressAccepted = false;
} else {
// Pointer is lifted before timeout.
resolve(GestureDisposition.rejected);
}
_reset();
} else if (event is PointerCancelEvent) {
_reset();
} else if (event is PointerDownEvent) {
} else if (event is PointerDownEvent || event is PointerCancelEvent) {
// The first touch.
_longPressAccepted = false;
_longPressOrigin = event.position;
_initialButtons = event.buttons;
} else if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
} else if (_longPressAccepted) {
_checkLongPressMoveUpdate(event);
}
}
}
void _checkLongPressStart() {
assert(_initialButtons == kPrimaryButton);
final LongPressStartDetails details = LongPressStartDetails(
globalPosition: _longPressOrigin,
);
if (onLongPressStart != null)
invokeCallback<void>('onLongPressStart',
() => onLongPressStart(details));
if (onLongPress != null)
invokeCallback<void>('onLongPress', onLongPress);
}
void _checkLongPressMoveUpdate(PointerEvent event) {
assert(_initialButtons == kPrimaryButton);
final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
globalPosition: event.position,
offsetFromOrigin: event.position - _longPressOrigin,
);
if (onLongPressMoveUpdate != null)
invokeCallback<void>('onLongPressMoveUpdate',
() => onLongPressMoveUpdate(details));
}
void _checkLongPressEnd(PointerEvent event) {
assert(_initialButtons == kPrimaryButton);
final LongPressEndDetails details = LongPressEndDetails(
globalPosition: event.position,
);
if (onLongPressEnd != null)
invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
if (onLongPressUp != null)
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
void _reset() {
_longPressAccepted = false;
_longPressOrigin = null;
_initialButtons = null;
}
@override
void resolve(GestureDisposition disposition) {
if (_longPressAccepted && disposition == GestureDisposition.rejected) {
// This can happen if the gesture has been canceled. For example when
// the buttons have changed.
_reset();
} else if (event is PointerMoveEvent && _longPressAccepted && onLongPressMoveUpdate != null) {
invokeCallback<void>('onLongPressMoveUpdate', () {
onLongPressMoveUpdate(LongPressMoveUpdateDetails(
globalPosition: event.position,
offsetFromOrigin: event.position - _longPressOrigin,
));
});
}
super.resolve(disposition);
}
@override
......
......@@ -16,10 +16,6 @@ import 'tap.dart';
/// Signature for callback when the user has tapped the screen at the same
/// location twice in quick succession.
///
/// See also:
///
/// * [GestureDetector.onDoubleTap], which matches this signature.
typedef GestureDoubleTapCallback = void Function();
/// Signature used by [MultiTapGestureRecognizer] for when a pointer that might
......@@ -64,16 +60,13 @@ class _TapTracker {
@required Duration doubleTapMinTime,
}) : assert(doubleTapMinTime != null),
assert(event != null),
assert(event.buttons != null),
pointer = event.pointer,
_initialPosition = event.position,
initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer;
final GestureArenaEntry entry;
final Offset _initialPosition;
final int initialButtons;
final _CountdownZoned _doubleTapMinTimeCountdown;
bool _isTrackingPointer = false;
......@@ -100,18 +93,10 @@ class _TapTracker {
bool hasElapsedMinTime() {
return _doubleTapMinTimeCountdown.timeout;
}
bool hasSameButton(PointerDownEvent event) {
return event.buttons == initialButtons;
}
}
/// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession.
///
/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
///
class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
///
......@@ -141,53 +126,26 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
// - The long timer between taps expires
// - The gesture arena decides we have been rejected wholesale
/// Called when the user has tapped the screen with a primary button at the
/// same location twice in quick succession.
///
/// This triggers when the pointer stops contacting the device after the 2nd tap,
/// immediately after [onDoubleTapUp].
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// Called when the user has tapped the screen at the same location twice in
/// quick succession.
GestureDoubleTapCallback onDoubleTap;
Timer _doubleTapTimer;
_TapTracker _firstTap;
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@override
bool isPointerAllowed(PointerEvent event) {
if (_firstTap == null) {
switch (event.buttons) {
case kPrimaryButton:
if (onDoubleTap == null)
return false;
break;
default:
return false;
}
}
return super.isPointerAllowed(event);
}
@override
void addAllowedPointer(PointerEvent event) {
if (_firstTap != null) {
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap.hasElapsedMinTime() || !_firstTap.hasSameButton(event)) {
// Restart when the second tap is too close to the first, or when buttons
// mismatch.
} else if (!_firstTap.hasElapsedMinTime()) {
// Restart when the second tap is too close to the first.
_reset();
return _trackFirstTap(event);
return addAllowedPointer(event);
}
}
_trackFirstTap(event);
}
void _trackFirstTap(PointerEvent event) {
_stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker(
event: event,
......@@ -277,7 +235,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
tracker.entry.resolve(GestureDisposition.accepted);
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
_checkUp(tracker.initialButtons);
if (onDoubleTap != null)
invokeCallback<void>('onDoubleTap', onDoubleTap);
_reset();
}
......@@ -301,12 +260,6 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
}
}
void _checkUp(int buttons) {
assert(buttons == kPrimaryButton);
if (onDoubleTap != null)
invokeCallback<void>('onDoubleTap', onDoubleTap);
}
@override
String get debugDescription => 'double tap';
}
......
This diff is collapsed.
......@@ -147,8 +147,7 @@ void main() {
});
test('TapGestureRecognizer _sentTapDown toString', () {
final TapGestureRecognizer tap = TapGestureRecognizer()
..onTap = () {}; // Add a callback so that event can be added
final TapGestureRecognizer tap = TapGestureRecognizer();
expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)'));
const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
tap.addPointer(event);
......
......@@ -75,7 +75,7 @@ void main() {
position: Offset(25.0, 25.0),
);
// Down/up pair 5: normal tap sequence identical to pair 1
// Down/up pair 5: normal tap sequence identical to pair 1 with different pointer
const PointerDownEvent down5 = PointerDownEvent(
pointer: 5,
position: Offset(10.0, 10.0),
......@@ -86,18 +86,6 @@ void main() {
position: Offset(11.0, 9.0),
);
// Down/up pair 6: normal tap sequence close to pair 1 but on secondary button
const PointerDownEvent down6 = PointerDownEvent(
pointer: 6,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const PointerUpEvent up6 = PointerUpEvent(
pointer: 6,
position: Offset(11.0, 9.0),
);
testGesture('Should recognize double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
......@@ -618,183 +606,4 @@ void main() {
tap.dispose();
});
group('Enforce consistent-button restriction:', () {
testGesture('Button change should interrupt existing sequence', (GestureTester tester) {
// Down1 -> down6 (different button from 1) -> down2 (same button as 1)
// Down1 and down2 could've been a double tap, but is interrupted by down 6.
const Duration interval = Duration(milliseconds: 100);
assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
tester.async.elapse(interval);
tap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(interval);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose();
});
testGesture('Button change should start a valid sequence', (GestureTester tester) {
// Down6 -> down1 (different button from 6) -> down2 (same button as 1)
const Duration interval = Duration(milliseconds: 100);
assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(interval);
tap.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(interval);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isTrue);
tap.dispose();
});
});
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that double tap recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
TapGestureRecognizer tapPrimary;
TapGestureRecognizer tapSecondary;
DoubleTapGestureRecognizer doubleTap;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognized.add('doubleTap');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
doubleTap.dispose();
});
testGesture('A primary double tap recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
doubleTap.addPointer(down6);
tapSecondary.addPointer(down6);
tester.closeArena(down6.pointer);
tester.route(down6);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary double tap recognizer forms competion with a primary tap recognizer', (GestureTester tester) {
doubleTap.addPointer(down1);
tapPrimary.addPointer(down1);
tester.closeArena(down1.pointer);
tester.route(down1);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 300));
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary double tap should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
..onDoubleTap = () {
recognized.add('primary');
};
// Down/up pair 7: normal tap sequence close to pair 6
const PointerDownEvent down7 = PointerDownEvent(
pointer: 7,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const PointerUpEvent up7 = PointerUpEvent(
pointer: 7,
position: Offset(11.0, 9.0),
);
doubleTap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(const Duration(milliseconds: 100));
doubleTap.addPointer(down7);
tester.closeArena(7);
tester.route(down7);
tester.route(up7);
expect(recognized, <String>[]);
recognized.clear();
doubleTap.dispose();
});
}
......@@ -20,20 +20,6 @@ void main() {
expect(nthStylusButton(2), kSecondaryStylusButton);
});
testGesture('smallestButton tests', (GestureTester tester) {
expect(smallestButton(0x0), equals(0x0));
expect(smallestButton(0x1), equals(0x1));
expect(smallestButton(0x200), equals(0x200));
expect(smallestButton(0x220), equals(0x20));
});
testGesture('isSingleButton tests', (GestureTester tester) {
expect(isSingleButton(0x0), isFalse);
expect(isSingleButton(0x1), isTrue);
expect(isSingleButton(0x200), isTrue);
expect(isSingleButton(0x220), isFalse);
});
group('Default values of PointerEvents:', () {
// Some parameters are intentionally set to a non-trivial value.
......
......@@ -318,38 +318,7 @@ void main() {
}
});
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', () {
test('Should not synthesise kPrimaryButton for certain devices', () {
final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
for (PointerDeviceKind kind in <PointerDeviceKind>[
PointerDeviceKind.mouse,
......
......@@ -7,7 +7,6 @@ import 'package:flutter/gestures.dart';
import '../flutter_test_alternative.dart';
import 'gesture_tester.dart';
// Down/move/up pair 1: normal tap sequence
const PointerDownEvent down = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
......@@ -23,29 +22,6 @@ const PointerMoveEvent move = PointerMoveEvent(
position: Offset(100, 200),
);
// Down/up pair 2: normal tap sequence far away from pair 1
const PointerDownEvent down2 = PointerDownEvent(
pointer: 6,
position: Offset(10, 10),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 6,
position: Offset(11, 9),
);
// Down/up pair 3: tap sequence with secondary button
const PointerDownEvent down3 = PointerDownEvent(
pointer: 7,
position: Offset(30, 30),
buttons: kSecondaryButton,
);
const PointerUpEvent up3 = PointerUpEvent(
pointer: 7,
position: Offset(31, 29),
);
void main() {
setUp(ensureGestureBinding);
......@@ -211,48 +187,6 @@ void main() {
longPress.dispose();
});
testGesture('Should not recognize long press with more than one buttons', (GestureTester tester) {
longPress.addPointer(const PointerDownEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton | kMiddleMouseButton,
position: Offset(10, 10),
));
tester.closeArena(5);
expect(longPressDown, isFalse);
tester.route(down);
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(longPressDown, isFalse);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
longPress.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
tester.route(down);
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
tester.route(const PointerMoveEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kMiddleMouseButton,
position: Offset(10, 10),
));
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 700));
expect(longPressDown, isFalse);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
});
});
group('long press drag', () {
......@@ -345,107 +279,6 @@ void main() {
});
});
group('Enforce consistent-button restriction:', () {
// In sequence between `down` and `up` but with buttons changed
const PointerMoveEvent moveR = PointerMoveEvent(
pointer: 5,
buttons: kSecondaryButton,
position: Offset(10, 10),
);
final List<String> recognized = <String>[];
LongPressGestureRecognizer longPress;
setUp(() {
longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('start');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('end');
};
});
tearDown(() {
longPress.dispose();
recognized.clear();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, <String>[]);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
expect(recognized, <String>[]);
});
testGesture('Buttons change before acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
recognized.clear();
// Second press
longPress.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
testGesture('Should cancel long press when buttons change after acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(moveR);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>[]);
});
testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(moveR);
tester.route(up);
recognized.clear();
// Second press
longPress.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
recognized.clear();
tester.route(up2);
expect(recognized, <String>['end']);
});
});
testGesture('Can filter long press based on device kind', (GestureTester tester) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse);
......@@ -485,107 +318,4 @@ void main() {
mouseLongPress.dispose();
});
group('Recognizers listening on different buttons do not form competition:', () {
// This test is assisted by tap recognizers. If a tap gesture has
// no competing recognizers, a pointer down event triggers its onTapDown
// immediately; if there are competitors, onTapDown is triggered after a
// timeout.
// The following tests make sure that long press recognizers do not form
// competition with a tap gesture recognizer listening on a different button.
final List<String> recognized = <String>[];
TapGestureRecognizer tapPrimary;
TapGestureRecognizer tapSecondary;
LongPressGestureRecognizer longPress;
setUp(() {
tapPrimary = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
recognized.add('tapPrimary');
};
tapSecondary = TapGestureRecognizer()
..onSecondaryTapDown = (TapDownDetails details) {
recognized.add('tapSecondary');
};
longPress = LongPressGestureRecognizer()
..onLongPressStart = (_) {
recognized.add('longPress');
};
});
tearDown(() {
recognized.clear();
tapPrimary.dispose();
tapSecondary.dispose();
longPress.dispose();
});
testGesture('A primary long press recognizer does not form competion with a secondary tap recognizer', (GestureTester tester) {
longPress.addPointer(down3);
tapSecondary.addPointer(down3);
tester.closeArena(down3.pointer);
tester.route(down3);
expect(recognized, <String>['tapSecondary']);
});
testGesture('A primary long press recognizer forms competion with a primary tap recognizer', (GestureTester tester) {
longPress.addPointer(down);
tapPrimary.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>[]);
tester.route(up);
expect(recognized, <String>['tapPrimary']);
});
});
testGesture('A secondary long press should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('primaryStart');
}
..onLongPress = () {
recognized.add('primary');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
recognized.add('primaryEnd');
}
..onLongPressUp = () {
recognized.add('primaryUp');
};
const PointerDownEvent down2 = PointerDownEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(30.0, 30.0),
);
const PointerMoveEvent move2 = PointerMoveEvent(
pointer: 2,
buttons: kSecondaryButton,
position: Offset(100, 200),
);
const PointerUpEvent up2 = PointerUpEvent(
pointer: 2,
position: Offset(100, 201),
);
longPress.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(move2);
tester.route(up2);
expect(recognized, <String>[]);
longPress.dispose();
recognized.clear();
});
}
......@@ -592,10 +592,7 @@ void main() {
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
() => VerticalDragGestureRecognizer(),
),
},
layoutDirection: TextDirection.ltr,
......@@ -1266,10 +1263,7 @@ void main() {
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
() => VerticalDragGestureRecognizer(),
),
},
layoutDirection: TextDirection.ltr,
......
......@@ -22,15 +22,9 @@ class TestPointer {
///
/// Multiple [TestPointer]s created with the same pointer identifier will
/// interfere with each other if they are used in parallel.
TestPointer([
this.pointer = 1,
this.kind = PointerDeviceKind.touch,
int buttons = kPrimaryButton,
])
TestPointer([this.pointer = 1, this.kind = PointerDeviceKind.touch])
: assert(kind != null),
assert(pointer != null),
assert(buttons != null),
_buttons = buttons;
assert(pointer != null);
/// The pointer identifier used for events generated by this object.
///
......@@ -41,11 +35,6 @@ class TestPointer {
/// [PointerDeviceKind.touch].
final PointerDeviceKind kind;
/// The kind of buttons to simulate on Down and Move events. Defaults to
/// [kPrimaryButton].
int get buttons => _buttons;
int _buttons;
/// Whether the pointer simulated by this object is currently down.
///
/// A pointer is released (goes up) by calling [up] or [cancel].
......@@ -62,14 +51,8 @@ class TestPointer {
/// If a custom event is created outside of this class, this function is used
/// to set the [isDown].
bool setDownInfo(
PointerEvent event,
Offset newLocation, {
int buttons,
}) {
bool setDownInfo(PointerEvent event, Offset newLocation) {
_location = newLocation;
if (buttons != null)
_buttons = buttons;
switch (event.runtimeType) {
case PointerDownEvent:
assert(!isDown);
......@@ -89,25 +72,15 @@ class TestPointer {
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
///
/// 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,
}) {
PointerDownEvent down(Offset newLocation, {Duration timeStamp = Duration.zero}) {
assert(!isDown);
_isDown = true;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerDownEvent(
timeStamp: timeStamp,
kind: kind,
pointer: pointer,
position: location,
buttons: _buttons,
);
}
......@@ -118,14 +91,7 @@ class TestPointer {
///
/// [isDown] must be true when this is called, since move events can only
/// be generated when the pointer is down.
///
/// 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,
}) {
PointerMoveEvent move(Offset newLocation, {Duration timeStamp = Duration.zero}) {
assert(
isDown,
'Move events can only be generated when the pointer is down. To '
......@@ -133,15 +99,12 @@ class TestPointer {
'up, use hover() instead.');
final Offset delta = newLocation - location;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerMoveEvent(
timeStamp: timeStamp,
kind: kind,
pointer: pointer,
position: newLocation,
delta: delta,
buttons: _buttons,
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment