diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index c70df59de1..517debf572 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -47,7 +47,8 @@ class ModalBarrier extends StatelessWidget { /// [ModalBarrier] built by [ModalRoute] pages. final Color? color; - /// Specifies if the barrier will be dismissed when the user taps on it. + /// Specifies if the barrier will be dismissed when the user taps or + /// performs a scroll gesture on it. /// /// If true, and [onDismiss] is non-null, [onDismiss] will be called, /// otherwise the current route will be popped from the ambient [Navigator]. @@ -228,12 +229,14 @@ class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer { : super(debugOwner: debugOwner); VoidCallback? onAnyTapUp; + VoidCallback? onAnyTapCancel; @protected @override bool isPointerAllowed(PointerDownEvent event) { - if (onAnyTapUp == null) + if (onAnyTapUp == null && onAnyTapCancel == null) { return false; + } return super.isPointerAllowed(event); } @@ -252,7 +255,7 @@ class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer { @protected @override void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) { - // Do nothing. + onAnyTapCancel?.call(); } @override @@ -272,9 +275,10 @@ class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate { class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> { - const _AnyTapGestureRecognizerFactory({this.onAnyTapUp}); + const _AnyTapGestureRecognizerFactory({this.onAnyTapUp, this.onAnyTapCancel}); final VoidCallback? onAnyTapUp; + final VoidCallback? onAnyTapCancel; @override _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer(); @@ -282,6 +286,7 @@ class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGe @override void initializer(_AnyTapGestureRecognizer instance) { instance.onAnyTapUp = onAnyTapUp; + instance.onAnyTapCancel = onAnyTapCancel; } } @@ -307,7 +312,7 @@ class _ModalBarrierGestureDetector extends StatelessWidget { @override Widget build(BuildContext context) { final Map gestures = { - _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss), + _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss, onAnyTapCancel: onDismiss), }; return RawGestureDetector( diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index 72d0664400..519e18bf87 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -251,6 +251,39 @@ void main() { ); }); + testWidgets('ModalBarrier pops the Navigator when dismissed by tap cancel', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => const FirstWidget(), + '/modal': (BuildContext context) => const SecondWidget(), + }; + + await tester.pumpWidget(MaterialApp(routes: routes)); + + // Initially the barrier is not visible + expect(find.byKey(const ValueKey('barrier')), findsNothing); + + // Tapping on X routes to the barrier + await tester.tap(find.text('X')); + await tester.pump(); // begin transition + await tester.pump(const Duration(seconds: 1)); // end transition + + // Press the barrier; it shouldn't dismiss yet + final TestGesture gesture = await tester.press( + find.byKey(const ValueKey('barrier')), + ); + await tester.pumpAndSettle(); // begin transition + expect(find.byKey(const ValueKey('barrier')), findsOneWidget); + + // Cancel the pointer; the barrier should be dismissed + await gesture.cancel(); + await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition + expect( + find.byKey(const ValueKey('barrier')), + findsNothing, + reason: 'The route should have been dismissed by tapping the barrier.', + ); + }); + testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => const FirstWidget(),