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;
}, },
); );
} }
......
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