Unverified Commit 2e1a8c74 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Add minimum time gap requirement to double tap (#28749)

* First impl with StopwatchWithZone

* Clean up params and name

* Remove outdated TODO

* Fix style

* Fix a missing param. Add @require

* Fix import meta

* Fix code style

* Add missing require. Fix comment style.

* Fix code style

* Fix code style
parent 1b14339e
......@@ -31,9 +31,7 @@ const Duration kLongPressTimeout = Duration(milliseconds: 500);
const Duration kDoubleTapTimeout = Duration(milliseconds: 300);
/// The minimum time from the end of the first tap to the start of the second
/// tap in a double-tap gesture. (Currently not honored by the
/// DoubleTapGestureRecognizer.)
// TODO(ianh): Either implement this or remove the constant.
/// tap in a double-tap gesture.
const Duration kDoubleTapMinTime = Duration(milliseconds: 40);
/// The maximum distance that the first touch in a double-tap gesture can travel
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart' show required;
import 'arena.dart';
import 'binding.dart';
......@@ -32,16 +33,41 @@ typedef GestureMultiTapCallback = void Function(int pointer);
/// [GestureMultiTapDownCallback] will not end up causing a tap.
typedef GestureMultiTapCancelCallback = void Function(int pointer);
/// CountdownZoned tracks whether the specified duration has elapsed since
/// creation, honoring [Zone].
class _CountdownZoned {
_CountdownZoned({ @required Duration duration })
: assert(duration != null) {
_timer = Timer(duration, _onTimeout);
}
bool _timeout = false;
Timer _timer;
bool get timeout => _timeout;
void _onTimeout() {
_timeout = true;
}
}
/// TapTracker helps track individual tap sequences as part of a
/// larger gesture.
class _TapTracker {
_TapTracker({ PointerDownEvent event, this.entry })
: pointer = event.pointer,
_initialPosition = event.position;
_TapTracker({
@required PointerDownEvent event,
this.entry,
@required Duration doubleTapMinTime,
}) : assert(doubleTapMinTime != null),
assert(event != null),
pointer = event.pointer,
_initialPosition = event.position,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer;
final GestureArenaEntry entry;
final Offset _initialPosition;
final _CountdownZoned _doubleTapMinTimeCountdown;
bool _isTrackingPointer = false;
......@@ -63,6 +89,10 @@ class _TapTracker {
final Offset offset = event.position - _initialPosition;
return offset.distance <= tolerance;
}
bool hasElapsedMinTime() {
return _doubleTapMinTimeCountdown.timeout;
}
}
/// Recognizes when the user has tapped the screen at the same location twice in
......@@ -106,14 +136,21 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
@override
void addAllowedPointer(PointerEvent event) {
// Ignore out-of-bounds second taps.
if (_firstTap != null &&
!_firstTap.isWithinTolerance(event, kDoubleTapSlop))
return;
if (_firstTap != null) {
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap.hasElapsedMinTime()) {
// Restart when the second tap is too close to the first.
_reset();
return addAllowedPointer(event);
}
}
_stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker(
event: event,
entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
doubleTapMinTime: kDoubleTapMinTime,
);
_trackers[event.pointer] = tracker;
tracker.startTrackingPointer(_handleEvent);
......@@ -239,7 +276,8 @@ class _TapGesture extends _TapTracker {
}) : _lastPosition = event.position,
super(
event: event,
entry: GestureBinding.instance.gestureArena.add(event.pointer, gestureRecognizer)
entry: GestureBinding.instance.gestureArena.add(event.pointer, gestureRecognizer),
doubleTapMinTime: kDoubleTapMinTime,
) {
startTrackingPointer(handleEvent);
if (longTapDelay > Duration.zero) {
......
......@@ -105,6 +105,7 @@ void main() {
GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down2);
tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
......@@ -364,6 +365,7 @@ void main() {
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down1);
tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
......@@ -525,4 +527,83 @@ void main() {
tap.dispose();
});
testGesture('Should not recognize two over-rapid taps', (GestureTester tester) {
final DoubleTapGestureRecognizer tap = DoubleTapGestureRecognizer();
bool doubleTapRecognized = false;
tap.onDoubleTap = () {
doubleTapRecognized = true;
};
tap.addPointer(down1);
tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 10));
tap.addPointer(down2);
tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tap.dispose();
});
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);
tester.closeArena(1);
expect(doubleTapRecognized, isFalse);
tester.route(down1);
expect(doubleTapRecognized, isFalse);
tester.route(up1);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(1);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 10));
tap.addPointer(down2);
tester.closeArena(2);
expect(doubleTapRecognized, isFalse);
tester.route(down2);
expect(doubleTapRecognized, isFalse);
tester.route(up2);
expect(doubleTapRecognized, isFalse);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
tester.async.elapse(const Duration(milliseconds: 100));
tap.addPointer(down5);
tester.closeArena(5);
expect(doubleTapRecognized, isFalse);
tester.route(down5);
expect(doubleTapRecognized, isFalse);
tester.route(up5);
expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(5);
expect(doubleTapRecognized, isTrue);
tap.dispose();
});
}
......@@ -52,6 +52,7 @@ void main() {
log.clear();
await tester.tap(find.byType(InkWell), pointer: 2);
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byType(InkWell), pointer: 3);
expect(log, equals(<String>['double-tap']));
......
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