diff --git a/packages/flutter/lib/src/gestures/debug.dart b/packages/flutter/lib/src/gestures/debug.dart index 59173dd79af..0bc9343e353 100644 --- a/packages/flutter/lib/src/gestures/debug.dart +++ b/packages/flutter/lib/src/gestures/debug.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:flutter/foundation.dart'; // Any changes to this file should be reflected in the debugAssertAllGesturesVarsUnset() diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart index 91a11c781ac..1c759c21612 100644 --- a/packages/flutter/lib/src/gestures/long_press.dart +++ b/packages/flutter/lib/src/gestures/long_press.dart @@ -645,7 +645,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { _initialButtons = event.buttons; _checkLongPressDown(event); } else if (event is PointerMoveEvent) { - if (event.buttons != _initialButtons) { + if (event.buttons != _initialButtons && !_longPressAccepted) { resolve(GestureDisposition.rejected); stopTrackingPointer(primaryPointer!); } else if (_longPressAccepted) { diff --git a/packages/flutter/test/gestures/long_press_test.dart b/packages/flutter/test/gestures/long_press_test.dart index 4de0cb46527..c967956a8b6 100644 --- a/packages/flutter/test/gestures/long_press_test.dart +++ b/packages/flutter/test/gestures/long_press_test.dart @@ -458,7 +458,7 @@ void main() { expect(recognized, ['end']); }); - testGesture('Should cancel long press when buttons change after acceptance', (GestureTester tester) { + testGesture('Should not cancel long press when buttons change after acceptance', (GestureTester tester) { // First press gesture.addPointer(down); tester.closeArena(down.pointer); @@ -470,7 +470,7 @@ void main() { tester.route(moveR); expect(recognized, []); tester.route(up); - expect(recognized, []); + expect(recognized, ['end']); }); testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) { @@ -701,4 +701,95 @@ void main() { longPress.dispose(); recognized.clear(); }); + + testGesture('Switching buttons mid-stream does not fail to send "end" event', (GestureTester tester) { + final List recognized = []; + final LongPressGestureRecognizer longPress = LongPressGestureRecognizer() + ..onLongPressStart = (LongPressStartDetails details) { + recognized.add('primaryStart'); + } + ..onLongPressEnd = (LongPressEndDetails details) { + recognized.add('primaryEnd'); + }; + + const PointerDownEvent down4 = PointerDownEvent( + pointer: 8, + position: Offset(10, 10), + ); + + const PointerMoveEvent move4 = PointerMoveEvent( + pointer: 8, + position: Offset(100, 200), + buttons: kPrimaryButton | kSecondaryButton, + ); + + const PointerUpEvent up4 = PointerUpEvent( + pointer: 8, + position: Offset(100, 200), + buttons: kSecondaryButton, + ); + + longPress.addPointer(down4); + tester.closeArena(4); + tester.route(down4); + tester.async.elapse(const Duration(milliseconds: 1000)); + recognized.add('two seconds later...'); + tester.route(move4); + tester.async.elapse(const Duration(milliseconds: 1000)); + recognized.add('two more seconds later...'); + tester.route(up4); + tester.async.elapse(const Duration(milliseconds: 1000)); + expect(recognized, ['primaryStart', 'two seconds later...', 'two more seconds later...', 'primaryEnd']); + longPress.dispose(); + }); + + testGesture('Switching buttons mid-stream does not fail to send "end" event (alternative sequence)', (GestureTester tester) { + // This reproduces sequences seen on macOS. + final List recognized = []; + final LongPressGestureRecognizer longPress = LongPressGestureRecognizer() + ..onLongPressStart = (LongPressStartDetails details) { + recognized.add('primaryStart'); + } + ..onLongPressEnd = (LongPressEndDetails details) { + recognized.add('primaryEnd'); + }; + + const PointerDownEvent down5 = PointerDownEvent( + pointer: 9, + position: Offset(10, 10), + ); + + const PointerMoveEvent move5a = PointerMoveEvent( + pointer: 9, + position: Offset(100, 200), + buttons: 3, // add 2 + ); + + const PointerMoveEvent move5b = PointerMoveEvent( + pointer: 9, + position: Offset(100, 200), + buttons: 2, // remove 1 + ); + + const PointerUpEvent up5 = PointerUpEvent( + pointer: 9, + position: Offset(100, 200), + ); + + longPress.addPointer(down5); + tester.closeArena(4); + tester.route(down5); + tester.async.elapse(const Duration(milliseconds: 1000)); + recognized.add('two seconds later...'); + tester.route(move5a); + tester.async.elapse(const Duration(milliseconds: 1000)); + recognized.add('two more seconds later...'); + tester.route(move5b); + tester.async.elapse(const Duration(milliseconds: 1000)); + recognized.add('two more seconds later still...'); + tester.route(up5); + tester.async.elapse(const Duration(milliseconds: 1000)); + expect(recognized, ['primaryStart', 'two seconds later...', 'two more seconds later...', 'two more seconds later still...', 'primaryEnd']); + longPress.dispose(); + }); }