| // 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 modifies the size of the [SemanticsNode.rect] created by its |
| /// child widget. |
| /// |
| /// It clips the focus in potentially four directions based on the |
| /// specified [EdgeInsets]. |
| /// |
| /// The size of the accessibility focus is adjusted based on value changes |
| /// inside the given [ValueNotifier]. |
| /// |
| /// See also: |
| /// |
| /// * [ModalBarrier], which utilizes this widget to adjust the barrier focus |
| /// size based on the size of the content layer rendered on top of it. |
| class _SemanticsClipper extends SingleChildRenderObjectWidget{ |
| /// creates a [SemanticsClipper] that updates the size of the |
| /// [SemanticsNode.rect] of its child based on the value inside the provided |
| /// [ValueNotifier], or a default value of [EdgeInsets.zero]. |
| const _SemanticsClipper({ |
| super.child, |
| required this.clipDetailsNotifier, |
| }); |
| |
| /// The [ValueNotifier] whose value determines how the child's |
| /// [SemanticsNode.rect] should be clipped in four directions. |
| final ValueNotifier<EdgeInsets> clipDetailsNotifier; |
| |
| @override |
| _RenderSemanticsClipper createRenderObject(BuildContext context) { |
| return _RenderSemanticsClipper(clipDetailsNotifier: clipDetailsNotifier,); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderSemanticsClipper renderObject) { |
| renderObject.clipDetailsNotifier = clipDetailsNotifier; |
| } |
| } |
| /// Updates the [SemanticsNode.rect] of its child based on the value inside |
| /// provided [ValueNotifier]. |
| class _RenderSemanticsClipper extends RenderProxyBox { |
| /// Creates a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child |
| /// based on the value inside provided [ValueNotifier]. |
| _RenderSemanticsClipper({ |
| required ValueNotifier<EdgeInsets> clipDetailsNotifier, |
| RenderBox? child, |
| }) : _clipDetailsNotifier = clipDetailsNotifier, |
| super(child); |
| |
| ValueNotifier<EdgeInsets> _clipDetailsNotifier; |
| |
| /// The getter and setter retrieves / updates the [ValueNotifier] associated |
| /// with this clipper. |
| ValueNotifier<EdgeInsets> get clipDetailsNotifier => _clipDetailsNotifier; |
| set clipDetailsNotifier (ValueNotifier<EdgeInsets> newNotifier) { |
| if (_clipDetailsNotifier == newNotifier) { |
| return; |
| } |
| if (attached) { |
| _clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate); |
| } |
| _clipDetailsNotifier = newNotifier; |
| _clipDetailsNotifier.addListener(markNeedsSemanticsUpdate); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| Rect get semanticBounds { |
| final EdgeInsets clipDetails = _clipDetailsNotifier.value; |
| final Rect originalRect = super.semanticBounds; |
| final Rect clippedRect = Rect.fromLTRB( |
| originalRect.left + clipDetails.left, |
| originalRect.top + clipDetails.top, |
| originalRect.right - clipDetails.right, |
| originalRect.bottom - clipDetails.bottom, |
| ); |
| return clippedRect; |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| clipDetailsNotifier.addListener(markNeedsSemanticsUpdate); |
| } |
| |
| @override |
| void detach() { |
| clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate); |
| super.detach(); |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| config.isSemanticBoundary = true; |
| } |
| } |
| |
| /// 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, |
| this.clipDetailsNotifier, |
| this.semanticsOnTapHint, |
| }); |
| |
| /// 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; |
| |
| /// {@template flutter.widgets.ModalBarrier.clipDetailsNotifier} |
| /// Contains a value of type [EdgeInsets] that specifies how the |
| /// [SemanticsNode.rect] of the widget should be clipped. |
| /// |
| /// See also: |
| /// |
| /// * [_SemanticsClipper], which utilizes the value inside to update the |
| /// [SemanticsNode.rect] for its child. |
| /// {@endtemplate} |
| final ValueNotifier<EdgeInsets>? clipDetailsNotifier; |
| |
| /// {@macro flutter.material.ModalBottomSheetRoute.barrierOnTapHint} |
| final String? semanticsOnTapHint; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context)); |
| final bool platformSupportsDismissingBarrier; |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| platformSupportsDismissingBarrier = false; |
| case TargetPlatform.android: |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| platformSupportsDismissingBarrier = true; |
| } |
| 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); |
| } |
| } |
| |
| Widget barrier = Semantics( |
| onTapHint: semanticsOnTapHint, |
| onTap: semanticsDismissible && semanticsLabel != null ? handleDismiss : null, |
| onDismiss: semanticsDismissible && semanticsLabel != null ? handleDismiss : null, |
| label: semanticsDismissible ? semanticsLabel : 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!, |
| ), |
| ), |
| ), |
| ); |
| |
| // Developers can set [dismissible: true] and [barrierSemanticsDismissible: true] |
| // to allow assistive technology users to dismiss a modal BottomSheet by |
| // tapping on the Scrim focus. |
| // On iOS, some modal barriers are not dismissible in accessibility mode. |
| final bool excluding = !semanticsDismissible || !modalBarrierSemanticsDismissible; |
| |
| if (!excluding && clipDetailsNotifier != null) { |
| barrier = _SemanticsClipper( |
| clipDetailsNotifier: clipDetailsNotifier!, |
| child: barrier, |
| ); |
| } |
| |
| return BlockSemantics( |
| child: ExcludeSemantics( |
| excluding: excluding, |
| child: _ModalBarrierGestureDetector( |
| onDismiss: handleDismiss, |
| child: barrier, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| /// 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, |
| this.clipDetailsNotifier, |
| this.semanticsOnTapHint, |
| }) : 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; |
| |
| /// {@macro flutter.widgets.ModalBarrier.clipDetailsNotifier} |
| final ValueNotifier<EdgeInsets>? clipDetailsNotifier; |
| |
| /// This hint text instructs users what they are able to do when they tap on |
| /// the [ModalBarrier] |
| /// |
| /// E.g. If the hint text is 'close bottom sheet", it will be announced as |
| /// "Double tap to close bottom sheet". |
| /// |
| /// If this value is null, the default onTapHint will be applied, resulting |
| /// in the announcement of 'Double tap to activate'. |
| final String? semanticsOnTapHint; |
| |
| @override |
| Widget build(BuildContext context) { |
| return ModalBarrier( |
| color: color.value, |
| dismissible: dismissible, |
| semanticsLabel: semanticsLabel, |
| barrierSemanticsDismissible: barrierSemanticsDismissible, |
| onDismiss: onDismiss, |
| clipDetailsNotifier: clipDetailsNotifier, |
| semanticsOnTapHint: semanticsOnTapHint, |
| ); |
| } |
| } |
| |
| // 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}) { |
| if (onAnyTapUp != null) { |
| invokeCallback('onAnyTapUp', onAnyTapUp!); |
| } |
| } |
| |
| @protected |
| @override |
| void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) { |
| // Do nothing. |
| } |
| |
| @override |
| String get debugDescription => 'any tap'; |
| } |
| |
| 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, |
| }); |
| |
| /// 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, |
| child: child, |
| ); |
| } |
| } |