Unverified Commit 3c366b70 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

`_TapStatusTrackerMixin` should wait until the next `PointerDownEvent` before...

`_TapStatusTrackerMixin` should wait until the next `PointerDownEvent` before resetting its state when the tap timer has elapsed (#129312)

`_TapStatusTrackerMixin` used by `BaseTapAndDragGestureRecognizer` should wait until the next tap down before resetting its state when the `_consecutiveTapTimer` times out. This is because `BaseTapAndDragGestureRecognizer` may not have fired its tap down/tap up event before the state has been reset preventing it from firing the tap down/tap up callbacks at all because `currentDown` and `currentUp` are reset to `null`.

Fixes #129161
parent 3df1de4c
......@@ -539,6 +539,9 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
if (_consecutiveTapTimer != null && !_consecutiveTapTimer!.isActive) {
_tapTrackerReset();
}
if (maxConsecutiveTap == _consecutiveTapCount) {
_tapTrackerReset();
}
......@@ -623,7 +626,7 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
}
void _consecutiveTapTimerStart() {
_consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _tapTrackerReset);
_consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _consecutiveTapTimerTimeout);
}
void _consecutiveTapTimerStop() {
......@@ -633,6 +636,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
}
}
void _consecutiveTapTimerTimeout() {
// The consecutive tap timer may time out before a tap down/tap up event is
// fired. In this case we should not reset the tap tracker state immediately.
// Instead we should reset the tap tracker on the next call to [addAllowedPointer],
// if the timer is no longer active.
}
void _tapTrackerReset() {
// The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent
// [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging
......
......@@ -2147,6 +2147,52 @@ void main() {
variant: TargetPlatformVariant.mobile(),
);
testWidgets('Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/129161.
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: GestureDetector(
onTap: () {},
onDoubleTap: () {},
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.up();
// This is to allow the GestureArena to decide a winner between TapGestureRecognizer,
// DoubleTapGestureRecognizer, and BaseTapAndDragGestureRecognizer.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('e'));
await gesture.down(ePos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
}, variant: TargetPlatformVariant.desktop());
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......
......@@ -694,6 +694,44 @@ void main() {
'panend#2']);
});
// This is a regression test for https://github.com/flutter/flutter/issues/129161.
testGesture('Beats TapGestureRecognizer and DoubleTapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TapGestureRecognizer taps = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
events.add('tapdown');
}
..onTapUp = (TapUpDetails details) {
events.add('tapup');
}
..onTapCancel = () {
events.add('tapscancel');
};
final DoubleTapGestureRecognizer doubleTaps = DoubleTapGestureRecognizer()
..onDoubleTapDown = (TapDownDetails details) {
events.add('doubletapdown');
}
..onDoubleTap = () {
events.add('doubletapup');
}
..onDoubleTapCancel = () {
events.add('doubletapcancel');
};
tapAndDrag.addPointer(down1);
taps.addPointer(down1);
doubleTaps.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
// Wait for GestureArena to resolve itself.
tester.async.elapse(kDoubleTapTimeout);
expect(events, <String>['down#1', 'up#1']);
});
testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
......
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