Unverified Commit 52b5b3ea authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add GestureDetector.onDoubleTapDown() (#64431)

* Add GestureDetector.onDoubleTapDown()

* Review comments
parent 8b52e6a8
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' show Offset; import 'dart:ui' show Offset;
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
...@@ -60,7 +59,7 @@ class _CountdownZoned { ...@@ -60,7 +59,7 @@ class _CountdownZoned {
class _TapTracker { class _TapTracker {
_TapTracker({ _TapTracker({
required PointerDownEvent event, required PointerDownEvent event,
this.entry, required this.entry,
required Duration doubleTapMinTime, required Duration doubleTapMinTime,
}) : assert(doubleTapMinTime != null), }) : assert(doubleTapMinTime != null),
assert(event != null), assert(event != null),
...@@ -71,7 +70,7 @@ class _TapTracker { ...@@ -71,7 +70,7 @@ class _TapTracker {
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime); _doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer; final int pointer;
final GestureArenaEntry? entry; final GestureArenaEntry entry;
final Offset _initialGlobalPosition; final Offset _initialGlobalPosition;
final int initialButtons; final int initialButtons;
final _CountdownZoned _doubleTapMinTimeCountdown; final _CountdownZoned _doubleTapMinTimeCountdown;
...@@ -122,25 +121,45 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -122,25 +121,45 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
}) : super(debugOwner: debugOwner, kind: kind); }) : super(debugOwner: debugOwner, kind: kind);
// Implementation notes: // Implementation notes:
//
// The double tap recognizer can be in one of four states. There's no // The double tap recognizer can be in one of four states. There's no
// explicit enum for the states, because they are already captured by // explicit enum for the states, because they are already captured by
// the state of existing fields. Specifically: // the state of existing fields. Specifically:
// Waiting on first tap: In this state, the _trackers list is empty, and //
// _firstTap is null. // 1. Waiting on first tap: In this state, the _trackers list is empty, and
// First tap in progress: In this state, the _trackers list contains all // _firstTap is null.
// the states for taps that have begun but not completed. This list can // 2. First tap in progress: In this state, the _trackers list contains all
// have more than one entry if two pointers begin to tap. // the states for taps that have begun but not completed. This list can
// Waiting on second tap: In this state, one of the in-progress taps has // have more than one entry if two pointers begin to tap.
// completed successfully. The _trackers list is again empty, and // 3. Waiting on second tap: In this state, one of the in-progress taps has
// _firstTap records the successful tap. // completed successfully. The _trackers list is again empty, and
// Second tap in progress: Much like the "first tap in progress" state, but // _firstTap records the successful tap.
// _firstTap is non-null. If a tap completes successfully while in this // 4. Second tap in progress: Much like the "first tap in progress" state, but
// state, the callback is called and the state is reset. // _firstTap is non-null. If a tap completes successfully while in this
// state, the callback is called and the state is reset.
//
// There are various other scenarios that cause the state to reset: // There are various other scenarios that cause the state to reset:
//
// - All in-progress taps are rejected (by time, distance, pointercancel, etc) // - All in-progress taps are rejected (by time, distance, pointercancel, etc)
// - 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
/// A pointer has contacted the screen with a primary button at the same
/// location twice in quick succession, which might be the start of a double
/// tap.
///
/// This triggers immediately after the down event of the second tap.
///
/// If this recognizer doesn't win the arena, [onDoubleTapCancel] is called
/// next. Otherwise, [onDoubleTap] is called next.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [TapDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onDoubleTapDown], which exposes this callback.
GestureTapDownCallback? onDoubleTapDown;
/// Called when the user has tapped the screen with a primary button at the /// Called when the user has tapped the screen with a primary button at the
/// same location twice in quick succession. /// same location twice in quick succession.
/// ///
...@@ -150,47 +169,71 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -150,47 +169,71 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
/// See also: /// See also:
/// ///
/// * [kPrimaryButton], the button this callback responds to. /// * [kPrimaryButton], the button this callback responds to.
/// * [GestureDetector.onDoubleTap], which exposes this callback.
GestureDoubleTapCallback? onDoubleTap; GestureDoubleTapCallback? onDoubleTap;
/// A pointer that previously triggered [onDoubleTapDown] will not end up
/// causing a double tap.
///
/// This triggers once the gesture loses the arena if [onDoubleTapDown] has
/// previously been triggered.
///
/// If this recognizer wins the arena, [onDoubleTap] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [GestureDetector.onDoubleTapCancel], which exposes this callback.
GestureTapCancelCallback? onDoubleTapCancel;
Timer? _doubleTapTimer; Timer? _doubleTapTimer;
_TapTracker? _firstTap; _TapTracker? _firstTap;
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{}; final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@override @override
bool isPointerAllowed(PointerEvent event) { bool isPointerAllowed(PointerDownEvent event) {
if (_firstTap == null) { if (_firstTap == null) {
switch (event.buttons) { switch (event.buttons) {
case kPrimaryButton: case kPrimaryButton:
if (onDoubleTap == null) if (onDoubleTapDown == null &&
onDoubleTap == null &&
onDoubleTapCancel == null)
return false; return false;
break; break;
default: default:
return false; return false;
} }
} }
return super.isPointerAllowed(event as PointerDownEvent); return super.isPointerAllowed(event);
} }
@override @override
void addAllowedPointer(PointerEvent event) { void addAllowedPointer(PointerDownEvent event) {
if (_firstTap != null) { if (_firstTap != null) {
if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) { if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps. // Ignore out-of-bounds second taps.
return; return;
} else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event as PointerDownEvent)) { } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
// Restart when the second tap is too close to the first, or when buttons // Restart when the second tap is too close to the first (touch screens
// mismatch. // often detect touches intermittently), or when buttons mismatch.
_reset(); _reset();
return _trackFirstTap(event); return _trackTap(event);
} else if (onDoubleTapDown != null) {
final TapDownDetails details = TapDownDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: getKindForPointer(event.pointer),
);
invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
} }
} }
_trackFirstTap(event); _trackTap(event);
} }
void _trackFirstTap(PointerEvent event) { void _trackTap(PointerDownEvent event) {
_stopDoubleTapTimer(); _stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker( final _TapTracker tracker = _TapTracker(
event: event as PointerDownEvent, event: event,
entry: GestureBinding.instance!.gestureArena.add(event.pointer, this), entry: GestureBinding.instance!.gestureArena.add(event.pointer, this),
doubleTapMinTime: kDoubleTapMinTime, doubleTapMinTime: kDoubleTapMinTime,
); );
...@@ -231,14 +274,17 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -231,14 +274,17 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
void _reject(_TapTracker tracker) { void _reject(_TapTracker tracker) {
_trackers.remove(tracker.pointer); _trackers.remove(tracker.pointer);
tracker.entry!.resolve(GestureDisposition.rejected); tracker.entry.resolve(GestureDisposition.rejected);
_freezeTracker(tracker); _freezeTracker(tracker);
// If the first tap is in progress, and we've run out of taps to track, if (_firstTap != null) {
// reset won't have any work to do. But if we're in the second tap, we need if (tracker == _firstTap) {
// to clear intermediate state. _reset();
if (_firstTap != null && } else {
(_trackers.isEmpty || tracker == _firstTap)) _checkCancel();
_reset(); if (_trackers.isEmpty)
_reset();
}
}
} }
@override @override
...@@ -250,6 +296,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -250,6 +296,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
void _reset() { void _reset() {
_stopDoubleTapTimer(); _stopDoubleTapTimer();
if (_firstTap != null) { if (_firstTap != null) {
if (_trackers.isNotEmpty)
_checkCancel();
// Note, order is important below in order for the resolve -> reject logic // Note, order is important below in order for the resolve -> reject logic
// to work properly. // to work properly.
final _TapTracker tracker = _firstTap!; final _TapTracker tracker = _firstTap!;
...@@ -272,8 +320,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -272,8 +320,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
} }
void _registerSecondTap(_TapTracker tracker) { void _registerSecondTap(_TapTracker tracker) {
_firstTap!.entry!.resolve(GestureDisposition.accepted); _firstTap!.entry.resolve(GestureDisposition.accepted);
tracker.entry!.resolve(GestureDisposition.accepted); tracker.entry.resolve(GestureDisposition.accepted);
_freezeTracker(tracker); _freezeTracker(tracker);
_trackers.remove(tracker.pointer); _trackers.remove(tracker.pointer);
_checkUp(tracker.initialButtons); _checkUp(tracker.initialButtons);
...@@ -306,6 +354,11 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -306,6 +354,11 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
invokeCallback<void>('onDoubleTap', onDoubleTap!); invokeCallback<void>('onDoubleTap', onDoubleTap!);
} }
void _checkCancel() {
if (onDoubleTapCancel != null)
invokeCallback<void>('onDoubleTapCancel', onDoubleTapCancel!);
}
@override @override
String get debugDescription => 'double tap'; String get debugDescription => 'double tap';
} }
...@@ -381,7 +434,7 @@ class _TapGesture extends _TapTracker { ...@@ -381,7 +434,7 @@ class _TapGesture extends _TapTracker {
if (_wonArena) if (_wonArena)
reject(); reject();
else else
entry!.resolve(GestureDisposition.rejected); // eventually calls reject() entry.resolve(GestureDisposition.rejected); // eventually calls reject()
} }
void _check() { void _check() {
......
...@@ -229,7 +229,9 @@ class GestureDetector extends StatelessWidget { ...@@ -229,7 +229,9 @@ class GestureDetector extends StatelessWidget {
this.onTertiaryTapDown, this.onTertiaryTapDown,
this.onTertiaryTapUp, this.onTertiaryTapUp,
this.onTertiaryTapCancel, this.onTertiaryTapCancel,
this.onDoubleTapDown,
this.onDoubleTap, this.onDoubleTap,
this.onDoubleTapCancel,
this.onLongPress, this.onLongPress,
this.onLongPressStart, this.onLongPressStart,
this.onLongPressMoveUpdate, this.onLongPressMoveUpdate,
...@@ -428,6 +430,20 @@ class GestureDetector extends StatelessWidget { ...@@ -428,6 +430,20 @@ class GestureDetector extends StatelessWidget {
/// * [kTertiaryButton], the button this callback responds to. /// * [kTertiaryButton], the button this callback responds to.
final GestureTapCancelCallback onTertiaryTapCancel; final GestureTapCancelCallback onTertiaryTapCancel;
/// A pointer that might cause a double tap has contacted the screen at a
/// particular location.
///
/// Triggered immediately after the down event of the second tap.
///
/// If the user completes the double tap and the gesture wins, [onDoubleTap]
/// will be called after this callback. Otherwise, [onDoubleTapCancel] will
/// be called after this callback.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapDownCallback onDoubleTapDown;
/// The user has tapped the screen with a primary button at the same location /// The user has tapped the screen with a primary button at the same location
/// twice in quick succession. /// twice in quick succession.
/// ///
...@@ -436,6 +452,14 @@ class GestureDetector extends StatelessWidget { ...@@ -436,6 +452,14 @@ class GestureDetector extends StatelessWidget {
/// * [kPrimaryButton], the button this callback responds to. /// * [kPrimaryButton], the button this callback responds to.
final GestureTapCallback onDoubleTap; final GestureTapCallback onDoubleTap;
/// The pointer that previously triggered [onDoubleTapDown] will not end up
/// causing a double tap.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback onDoubleTapCancel;
/// Called when a long press gesture with a primary button 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
...@@ -774,7 +798,10 @@ class GestureDetector extends StatelessWidget { ...@@ -774,7 +798,10 @@ class GestureDetector extends StatelessWidget {
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>( gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this), () => DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) { (DoubleTapGestureRecognizer instance) {
instance.onDoubleTap = onDoubleTap; instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap
..onDoubleTapCancel = onDoubleTapCancel;
}, },
); );
} }
......
...@@ -26,7 +26,37 @@ class TestGestureArenaMember extends GestureArenaMember { ...@@ -26,7 +26,37 @@ class TestGestureArenaMember extends GestureArenaMember {
} }
void main() { void main() {
setUp(ensureGestureBinding); DoubleTapGestureRecognizer tap;
bool doubleTapRecognized;
TapDownDetails doubleTapDownDetails;
bool doubleTapCanceled;
setUp(() {
ensureGestureBinding();
tap = DoubleTapGestureRecognizer();
doubleTapRecognized = false;
tap.onDoubleTap = () {
expect(doubleTapRecognized, isFalse);
doubleTapRecognized = true;
};
doubleTapDownDetails = null;
tap.onDoubleTapDown = (TapDownDetails details) {
expect(doubleTapDownDetails, isNull);
doubleTapDownDetails = details;
};
doubleTapCanceled = false;
tap.onDoubleTapCancel = () {
expect(doubleTapCanceled, isFalse);
doubleTapCanceled = true;
};
});
tearDown(() {
tap.dispose();
});
// Down/up pair 1: normal tap sequence // Down/up pair 1: normal tap sequence
const PointerDownEvent down1 = PointerDownEvent( const PointerDownEvent down1 = PointerDownEvent(
...@@ -101,524 +131,330 @@ void main() { ...@@ -101,524 +131,330 @@ void main() {
); );
testGesture('Should recognize double tap', (GestureTester tester) { testGesture('Should recognize double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100)); tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down2.position);
expect(doubleTapDownDetails.localPosition, down2.localPosition);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isTrue); expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
testGesture('Inter-tap distance cancels double tap', (GestureTester tester) { testGesture('Inter-tap distance cancels double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down3); tap.addPointer(down3);
tester.closeArena(3); tester.closeArena(3);
expect(doubleTapRecognized, isFalse);
tester.route(down3); tester.route(down3);
expect(doubleTapRecognized, isFalse);
tester.route(up3); tester.route(up3);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(3); GestureBinding.instance.gestureArena.sweep(3);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Intra-tap distance cancels double tap', (GestureTester tester) { testGesture('Intra-tap distance cancels double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down4); tap.addPointer(down4);
tester.closeArena(4); tester.closeArena(4);
expect(doubleTapRecognized, isFalse);
tester.route(down4); tester.route(down4);
expect(doubleTapRecognized, isFalse);
tester.route(move4); tester.route(move4);
expect(doubleTapRecognized, isFalse);
tester.route(up4); tester.route(up4);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(4); GestureBinding.instance.gestureArena.sweep(4);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Inter-tap delay cancels double tap', (GestureTester tester) { testGesture('Inter-tap delay cancels double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 5000)); tester.async.elapse(const Duration(milliseconds: 5000));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Inter-tap delay resets double tap, allowing third tap to be a double-tap', (GestureTester tester) { testGesture('Inter-tap delay resets double tap, allowing third tap to be a double-tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 5000)); tester.async.elapse(const Duration(milliseconds: 5000));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100)); tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down5); tap.addPointer(down5);
tester.closeArena(5); tester.closeArena(5);
expect(doubleTapRecognized, isFalse);
tester.route(down5); tester.route(down5);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down5.position);
expect(doubleTapDownDetails.localPosition, down5.localPosition);
tester.route(up5); tester.route(up5);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(doubleTapRecognized, isTrue); expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
testGesture('Intra-tap delay does not cancel double tap', (GestureTester tester) { testGesture('Intra-tap delay does not cancel double tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 1000)); tester.async.elapse(const Duration(milliseconds: 1000));
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down2.position);
expect(doubleTapDownDetails.localPosition, down2.localPosition);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isTrue); expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
testGesture('Should not recognize two overlapping taps', (GestureTester tester) { testGesture('Should not recognize two overlapping taps', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Should recognize one tap of group followed by second tap', (GestureTester tester) { testGesture('Should recognize one tap of group followed by second tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100)); tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down1.position);
expect(doubleTapDownDetails.localPosition, down1.localPosition);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isTrue); expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
testGesture('Should cancel on arena reject during first tap', (GestureTester tester) { testGesture('Should cancel on arena reject during first tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
final TestGestureArenaMember member = TestGestureArenaMember(); final TestGestureArenaMember member = TestGestureArenaMember();
final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member); final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
entry.resolve(GestureDisposition.accepted); entry.resolve(GestureDisposition.accepted);
expect(member.accepted, isTrue); expect(member.accepted, isTrue);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Should cancel on arena reject between taps', (GestureTester tester) { testGesture('Should cancel on arena reject between taps', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
final TestGestureArenaMember member = TestGestureArenaMember(); final TestGestureArenaMember member = TestGestureArenaMember();
final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member); final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
entry.resolve(GestureDisposition.accepted); entry.resolve(GestureDisposition.accepted);
expect(member.accepted, isTrue); expect(member.accepted, isTrue);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Should cancel on arena reject during last tap', (GestureTester tester) { testGesture('Should cancel on arena reject during last tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
final TestGestureArenaMember member = TestGestureArenaMember(); final TestGestureArenaMember member = TestGestureArenaMember();
final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member); final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down2.position);
expect(doubleTapDownDetails.localPosition, down2.localPosition);
expect(doubleTapCanceled, isFalse);
entry.resolve(GestureDisposition.accepted); entry.resolve(GestureDisposition.accepted);
expect(member.accepted, isTrue); expect(member.accepted, isTrue);
expect(doubleTapCanceled, isTrue);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
tap.dispose();
}); });
testGesture('Passive gesture should trigger on double tap cancel', (GestureTester tester) { testGesture('Passive gesture should trigger on double tap cancel', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
FakeAsync().run((FakeAsync async) { FakeAsync().run((FakeAsync async) {
tap.addPointer(down1); tap.addPointer(down1);
final TestGestureArenaMember member = TestGestureArenaMember(); final TestGestureArenaMember member = TestGestureArenaMember();
GestureBinding.instance.gestureArena.add(1, member); GestureBinding.instance.gestureArena.add(1, member);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
expect(member.accepted, isFalse); expect(member.accepted, isFalse);
async.elapse(const Duration(milliseconds: 5000)); async.elapse(const Duration(milliseconds: 5000));
expect(member.accepted, isTrue); expect(member.accepted, isTrue);
});
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
});
}); });
testGesture('Should not recognize two over-rapid taps', (GestureTester tester) { testGesture('Should not recognize two over-rapid taps', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 10)); tester.async.elapse(const Duration(milliseconds: 10));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose(); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
}); });
testGesture('Over-rapid taps resets double tap, allowing third tap to be a double-tap', (GestureTester tester) { testGesture('Over-rapid taps resets double tap, allowing third tap to be a double-tap', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1); tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1); tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 10)); tester.async.elapse(const Duration(milliseconds: 10));
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2); tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2); tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100)); tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down5); tap.addPointer(down5);
tester.closeArena(5); tester.closeArena(5);
expect(doubleTapRecognized, isFalse);
tester.route(down5); tester.route(down5);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down5.position);
expect(doubleTapDownDetails.localPosition, down5.localPosition);
tester.route(up5); tester.route(up5);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(doubleTapRecognized, isTrue); expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
group('Enforce consistent-button restriction:', () { group('Enforce consistent-button restriction:', () {
...@@ -630,13 +466,6 @@ void main() { ...@@ -630,13 +466,6 @@ void main() {
assert(interval * 2 < kDoubleTapTimeout); assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime); assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1); tap.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -661,8 +490,8 @@ void main() { ...@@ -661,8 +490,8 @@ void main() {
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
tap.dispose(); expect(doubleTapCanceled, isFalse);
}); });
testGesture('Button change should start a valid sequence', (GestureTester tester) { testGesture('Button change should start a valid sequence', (GestureTester tester) {
...@@ -672,13 +501,6 @@ void main() { ...@@ -672,13 +501,6 @@ void main() {
assert(interval * 2 < kDoubleTapTimeout); assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime); assert(interval > kDoubleTapMinTime);
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down6); tap.addPointer(down6);
tester.closeArena(6); tester.closeArena(6);
tester.route(down6); tester.route(down6);
...@@ -694,17 +516,20 @@ void main() { ...@@ -694,17 +516,20 @@ void main() {
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse); expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
tester.async.elapse(interval); tester.async.elapse(interval);
tap.addPointer(down2); tap.addPointer(down2);
tester.closeArena(2); tester.closeArena(2);
tester.route(down2); tester.route(down2);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails.globalPosition, down2.position);
expect(doubleTapDownDetails.localPosition, down2.localPosition);
tester.route(up2); tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2); GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isTrue); expect(doubleTapRecognized, isTrue);
expect(doubleTapCanceled, isFalse);
tap.dispose();
}); });
}); });
......
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