// Copyright 2014 The Flutter Authors. All rights reserved. // 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'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'gesture_detector.dart'; import 'navigator.dart'; import 'transitions.dart'; /// A widget that prevents the user from interacting with widgets behind itself. /// /// The modal barrier is the scrim that is rendered behind each route, which /// generally prevents the user from interacting with the route below the /// current route, and normally partially obscures such routes. /// /// For example, when a dialog is on the screen, the page below the dialog is /// usually darkened by the modal barrier. /// /// See also: /// /// * [ModalRoute], which indirectly uses this widget. /// * [AnimatedModalBarrier], which is similar but takes an animated [color] /// instead of a single color value. class ModalBarrier extends StatelessWidget { /// Creates a widget that blocks user interaction. const ModalBarrier({ super.key, this.color, this.dismissible = true, this.onDismiss, this.semanticsLabel, this.barrierSemanticsDismissible = true, }); /// If non-null, fill the barrier with this color. /// /// See also: /// /// * [ModalRoute.barrierColor], which controls this property for the /// [ModalBarrier] built by [ModalRoute] pages. final Color? color; /// Specifies if the barrier will be dismissed when the user taps on it. /// /// If true, and [onDismiss] is non-null, [onDismiss] will be called, /// otherwise the current route will be popped from the ambient [Navigator]. /// /// If false, tapping on the barrier will do nothing. /// /// See also: /// /// * [ModalRoute.barrierDismissible], which controls this property for the /// [ModalBarrier] built by [ModalRoute] pages. final bool dismissible; /// {@template flutter.widgets.ModalBarrier.onDismiss} /// Called when the barrier is being dismissed. /// /// If non-null [onDismiss] will be called in place of popping the current /// route. It is up to the callback to handle dismissing the barrier. /// /// If null, the ambient [Navigator]'s current route will be popped. /// /// This field is ignored if [dismissible] is false. /// {@endtemplate} final VoidCallback? onDismiss; /// Whether the modal barrier semantics are included in the semantics tree. /// /// See also: /// /// * [ModalRoute.semanticsDismissible], which controls this property for /// the [ModalBarrier] built by [ModalRoute] pages. final bool? barrierSemanticsDismissible; /// Semantics label used for the barrier if it is [dismissible]. /// /// The semantics label is read out by accessibility tools (e.g. TalkBack /// on Android and VoiceOver on iOS) when the barrier is focused. /// /// See also: /// /// * [ModalRoute.barrierLabel], which controls this property for the /// [ModalBarrier] built by [ModalRoute] pages. final String? semanticsLabel; @override Widget build(BuildContext context) { assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context)); final bool platformSupportsDismissingBarrier; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: platformSupportsDismissingBarrier = false; break; case TargetPlatform.iOS: case TargetPlatform.macOS: platformSupportsDismissingBarrier = true; break; } assert(platformSupportsDismissingBarrier != null); final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier; final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible; void handleDismiss() { if (dismissible) { if (onDismiss != null) { onDismiss!(); } else { Navigator.maybePop(context); } } else { SystemSound.play(SystemSoundType.alert); } } return BlockSemantics( child: ExcludeSemantics( // On Android, the back button is used to dismiss a modal. On iOS, some // modal barriers are not dismissible in accessibility mode. excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible, child: _ModalBarrierGestureDetector( onDismiss: handleDismiss, child: Semantics( label: semanticsDismissible ? semanticsLabel : null, onDismiss: semanticsDismissible ? handleDismiss : null, textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, child: MouseRegion( cursor: SystemMouseCursors.basic, child: ConstrainedBox( constraints: const BoxConstraints.expand(), child: color == null ? null : ColoredBox( color: color!, ), ), ), ), ), ), ); } } /// A widget that prevents the user from interacting with widgets behind itself, /// and can be configured with an animated color value. /// /// The modal barrier is the scrim that is rendered behind each route, which /// generally prevents the user from interacting with the route below the /// current route, and normally partially obscures such routes. /// /// For example, when a dialog is on the screen, the page below the dialog is /// usually darkened by the modal barrier. /// /// This widget is similar to [ModalBarrier] except that it takes an animated /// [color] instead of a single color. /// /// See also: /// /// * [ModalRoute], which uses this widget. class AnimatedModalBarrier extends AnimatedWidget { /// Creates a widget that blocks user interaction. const AnimatedModalBarrier({ super.key, required Animation<Color?> color, this.dismissible = true, this.semanticsLabel, this.barrierSemanticsDismissible, this.onDismiss, }) : super(listenable: color); /// If non-null, fill the barrier with this color. /// /// See also: /// /// * [ModalRoute.barrierColor], which controls this property for the /// [AnimatedModalBarrier] built by [ModalRoute] pages. Animation<Color?> get color => listenable as Animation<Color?>; /// Whether touching the barrier will pop the current route off the [Navigator]. /// /// See also: /// /// * [ModalRoute.barrierDismissible], which controls this property for the /// [AnimatedModalBarrier] built by [ModalRoute] pages. final bool dismissible; /// Semantics label used for the barrier if it is [dismissible]. /// /// The semantics label is read out by accessibility tools (e.g. TalkBack /// on Android and VoiceOver on iOS) when the barrier is focused. /// See also: /// /// * [ModalRoute.barrierLabel], which controls this property for the /// [ModalBarrier] built by [ModalRoute] pages. final String? semanticsLabel; /// Whether the modal barrier semantics are included in the semantics tree. /// /// See also: /// /// * [ModalRoute.semanticsDismissible], which controls this property for /// the [ModalBarrier] built by [ModalRoute] pages. final bool? barrierSemanticsDismissible; /// {@macro flutter.widgets.ModalBarrier.onDismiss} final VoidCallback? onDismiss; @override Widget build(BuildContext context) { return ModalBarrier( color: color.value, dismissible: dismissible, semanticsLabel: semanticsLabel, barrierSemanticsDismissible: barrierSemanticsDismissible, onDismiss: onDismiss, ); } } // Recognizes tap down by any pointer button. // // It is similar to [TapGestureRecognizer.onTapDown], but accepts any single // button, which means the gesture also takes parts in gesture arenas. class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer { _AnyTapGestureRecognizer(); VoidCallback? onAnyTapUp; @protected @override bool isPointerAllowed(PointerDownEvent event) { if (onAnyTapUp == null) { return false; } return super.isPointerAllowed(event); } @protected @override void handleTapDown({PointerDownEvent? down}) { // Do nothing. } @protected @override void handleTapUp({PointerDownEvent? down, PointerUpEvent? up}) { onAnyTapUp?.call(); } @protected @override void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) { // Do nothing. } @override String get debugDescription => 'any tap'; } class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate { const _ModalBarrierSemanticsDelegate({this.onDismiss}); final VoidCallback? onDismiss; @override void assignSemantics(RenderSemanticsGestureHandler renderObject) { renderObject.onTap = onDismiss; } } class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> { const _AnyTapGestureRecognizerFactory({this.onAnyTapUp}); final VoidCallback? onAnyTapUp; @override _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer(); @override void initializer(_AnyTapGestureRecognizer instance) { instance.onAnyTapUp = onAnyTapUp; } } // A GestureDetector used by ModalBarrier. It only has one callback, // [onAnyTapDown], which recognizes tap down unconditionally. class _ModalBarrierGestureDetector extends StatelessWidget { const _ModalBarrierGestureDetector({ required this.child, required this.onDismiss, }) : assert(child != null), assert(onDismiss != null); /// The widget below this widget in the tree. /// See [RawGestureDetector.child]. final Widget child; /// Immediately called when an event that should dismiss the modal barrier /// has happened. final VoidCallback onDismiss; @override Widget build(BuildContext context) { final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{ _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss), }; return RawGestureDetector( gestures: gestures, behavior: HitTestBehavior.opaque, semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss), child: child, ); } }