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