From bd938b0085ae92d315b802277ece1c9903b6f808 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Thu, 12 Jan 2023 17:55:04 -0800 Subject: [PATCH] Fix tap/drag callbacks firing when TapAndDragGestureRecognizer has not won the arena (#118342) * Prevent drag and tap from accepting when a tap down exceeds the recognizers deadline but the recognizer has not won the arena * Add test * make analyzer happy Co-authored-by: Renzo Olivares --- .../src/widgets/tap_and_drag_gestures.dart | 15 ++++-- .../test/material/text_field_test.dart | 51 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart index e56f7ca074b6..6641fc5490ed 100644 --- a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart +++ b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart @@ -850,6 +850,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap // Tap related state. bool _pastSlopTolerance = false; bool _sentTapDown = false; + bool _wonArenaForPrimaryPointer = false; // Primary pointer being tracked by this recognizer. int? _primaryPointer; @@ -934,7 +935,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap void handleNonAllowedPointer(PointerDownEvent event) { // There can be multiple drags simultaneously. Their effects are combined. if (event.buttons != kPrimaryButton) { - if (!_sentTapDown) { + if (!_wonArenaForPrimaryPointer) { super.handleNonAllowedPointer(event); } } @@ -956,6 +957,8 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap _checkTapDown(currentDown!); } + _wonArenaForPrimaryPointer = true; + if (_start != null) { _acceptDrag(_start!); } @@ -976,7 +979,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap case _DragState.possible: if (_pastSlopTolerance) { // This means the pointer was not accepted as a tap. - if (_sentTapDown) { + if (_wonArenaForPrimaryPointer) { // If the recognizer has already won the arena for the primary pointer being tracked // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a // drag gesture. @@ -1043,7 +1046,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap // This can occur when the recognizer is accepted before a [PointerMoveEvent] has been // received that moves the pointer a sufficient global distance to be considered a drag. - if (_start != null && _sentTapDown) { + if (_start != null) { _acceptDrag(_start!); } } @@ -1085,6 +1088,9 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap String get debugDescription => 'tap_and_drag'; void _acceptDrag(PointerEvent event) { + if (!_wonArenaForPrimaryPointer) { + return; + } _dragState = _DragState.accepted; if (dragStartBehavior == DragStartBehavior.start) { _initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta); @@ -1138,7 +1144,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap } void _checkTapUp(PointerUpEvent event) { - if (!_sentTapDown) { + if (!_wonArenaForPrimaryPointer) { return; } @@ -1265,6 +1271,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap void _resetTaps() { _sentTapDown = false; + _wonArenaForPrimaryPointer = false; _primaryPointer = null; } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 9b55f0ebadeb..e4414e8291ca 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -12452,6 +12452,57 @@ void main() { }); }); + testWidgets('TextField does not leak touch events when deadline has exceeded', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/118340. + int textFieldTapCount = 0; + int prefixTapCount = 0; + int suffixTapCount = 0; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextField( + onTap: () { textFieldTapCount += 1; }, + decoration: InputDecoration( + labelText: 'Label', + prefix: ElevatedButton( + onPressed: () { prefixTapCount += 1; }, + child: const Text('prefix'), + ), + suffix: ElevatedButton( + onPressed: () { suffixTapCount += 1; }, + child: const Text('suffix'), + ), + ), + ), + ), + ), + ); + + TestGesture gesture = + await tester.startGesture( + tester.getRect(find.text('prefix')).center, + pointer: 7, + kind: PointerDeviceKind.mouse, + ); + await tester.pumpAndSettle(); + await gesture.up(); + expect(textFieldTapCount, 0); + expect(prefixTapCount, 1); + expect(suffixTapCount, 0); + + gesture = await tester.startGesture( + tester.getRect(find.text('suffix')).center, + pointer: 7, + kind: PointerDeviceKind.mouse, + ); + await tester.pumpAndSettle(); + await gesture.up(); + expect(textFieldTapCount, 0); + expect(prefixTapCount, 1); + expect(suffixTapCount, 1); + }); + testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/39376.