diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 5857aac82d..e999c2906d 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -365,6 +365,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { localPosition: correctedPosition.local, ); } + // This acceptGesture might have been called only for one pointer, instead + // of all pointers. Resolve all pointers to `accepted`. This won't cause + // infinite recursion because an accepted pointer won't be accepted again. + resolve(GestureDisposition.accepted); } } diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index b0ff102c5c..f97e1b495d 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -1440,4 +1440,48 @@ void main() { logs.clear(); }, ); + + testGesture('Does not crash when one of the 2 pointers wins by default and is then released', (GestureTester tester) { + // Regression test for https://github.com/flutter/flutter/issues/82784 + + bool didStartDrag = false; + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() + ..onStart = (_) { didStartDrag = true; } + ..onEnd = (DragEndDetails details) {} // Crash triggers at onEnd. + ..dragStartBehavior = DragStartBehavior.down; + final TapGestureRecognizer tap = TapGestureRecognizer()..onTap = () {}; + final TapGestureRecognizer tap2 = TapGestureRecognizer()..onTap = () {}; + + // The pointer1 is caught by drag and tap. + final TestPointer pointer1 = TestPointer(5); + final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0)); + drag.addPointer(down1); + tap.addPointer(down1); + tester.closeArena(pointer1.pointer); + tester.route(down1); + + // The pointer2 is caught by drag and tap2. + final TestPointer pointer2 = TestPointer(6); + final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 10.0)); + drag.addPointer(down2); + tap2.addPointer(down2); + tester.closeArena(pointer2.pointer); + tester.route(down2); + + // The tap is disposed, leaving drag the default winner. + tap.dispose(); + + // Wait for microtasks to finish, during which drag claims victory. + tester.async.flushMicrotasks(); + expect(didStartDrag, true); + + // The pointer1 is released, leaving pointer2 drag's only pointer. + tester.route(pointer1.up()); + + drag.dispose(); + + // Passes if no crashes here. + + tap2.dispose(); + }); }