diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index bc2f630386b..04da2bbb715 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -546,21 +546,35 @@ class _SlidingTapGestureRecognizer extends VerticalDragGestureRecognizer { if (event is PointerMoveEvent) { onResponsiveUpdate?.call(event.position); } - // If this gesture has a competing gesture (such as scrolling), and the - // pointer has not moved far enough to get this panning accepted, a - // pointer up event should still be considered as an accepted tap up. - // Manually accept this gesture here, which triggers onDragEnd. + // Sliding tap needs to handle 'up' events differently compared to typical + // drag gestures. If there's another gesture recognizer (like scrolling) + // competing and the pointer hasn't moved beyond the tolerance limit + // (slop), this gesture must still be accepted. + // + // Simply calling `accept()` here to handle this won't work because it + // would break backward compatibility with legacy buttons (see + // https://github.com/flutter/flutter/issues/150980 for more details). + // Legacy buttons recognize taps using `GestureDetector.onTap`, which + // neither accepts nor rejects for short taps. Instead, they wait for the + // default resolution as the last contender in the gesture arena. + // + // Therefore, this gesture should also follow the same strategy of not + // immediately accepting or rejecting. This allows tap gestures to take + // precedence for being inner, while sliding taps can take precedence over + // scroll gestures when the latter give up. if (event is PointerUpEvent) { - resolve(GestureDisposition.accepted); stopTrackingPointer(_primaryPointer!); onResponsiveEnd?.call(event.position); - } else { - super.handleEvent(event); + _primaryPointer = null; + // Do not call `super.handleEvent`, which gives up the pointer and thus + // rejects the gesture. + return; } - if (event is PointerUpEvent || event is PointerCancelEvent) { + if (event is PointerCancelEvent) { _primaryPointer = null; } } + super.handleEvent(event); } @override diff --git a/packages/flutter/test/cupertino/action_sheet_test.dart b/packages/flutter/test/cupertino/action_sheet_test.dart index 1903f349e4d..80319a8027f 100644 --- a/packages/flutter/test/cupertino/action_sheet_test.dart +++ b/packages/flutter/test/cupertino/action_sheet_test.dart @@ -1155,6 +1155,52 @@ void main() { expect(pressed, null); }); + testWidgets('Taps on legacy button calls onPressed and renders correctly', (WidgetTester tester) async { + // Legacy buttons are implemented with [GestureDetector.onTap]. Apps that + // use customized legacy buttons should continue to work. + // + // Regression test for https://github.com/flutter/flutter/issues/150980 . + bool wasPressed = false; + await tester.pumpWidget( + createAppWithButtonThatLaunchesActionSheet( + Builder(builder: (BuildContext context) { + return CupertinoActionSheet( + actions: [ + LegacyAction( + child: const Text('Legacy'), + onPressed: () { + expect(wasPressed, false); + wasPressed = true; + Navigator.pop(context); + }, + ), + CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}), + CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}), + ], + ); + }), + ), + ); + + await tester.tap(find.text('Go')); + await tester.pumpAndSettle(); + expect(wasPressed, isFalse); + + // Push the legacy button and hold for a while to activate the pressing effect. + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Legacy'))); + await tester.pump(const Duration(seconds: 1)); + expect(wasPressed, isFalse); + await expectLater( + find.byType(CupertinoActionSheet), + matchesGoldenFile('cupertinoActionSheet.legacyButton.png'), + ); + + await gesture.up(); + await tester.pumpAndSettle(); + expect(wasPressed, isTrue); + expect(find.text('Legacy'), findsNothing); + }); + testWidgets('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( @@ -2054,3 +2100,32 @@ class OverrideMediaQuery extends StatelessWidget { ); } } + +// Old-style action sheet buttons, which are implemented with +// `GestureDetector.onTap`. +class LegacyAction extends StatelessWidget { + const LegacyAction({ + super.key, + required this.onPressed, + required this.child, + }); + + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + behavior: HitTestBehavior.opaque, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 57), + child: Container( + alignment: AlignmentDirectional.center, + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0), + child: child, + ), + ), + ); + } +}