| // Copyright 2015 The Chromium 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 'basic.dart'; |
| import 'container.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({ |
| Key key, |
| this.color, |
| this.dismissible = true, |
| this.semanticsLabel, |
| this.barrierSemanticsDismissible = true, |
| }) : super(key: key); |
| |
| /// 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; |
| |
| /// Whether touching the barrier will pop the current route off the [Navigator]. |
| /// |
| /// See also: |
| /// |
| /// * [ModalRoute.barrierDismissible], which controls this property for the |
| /// [ModalBarrier] built by [ModalRoute] pages. |
| final bool dismissible; |
| |
| /// 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)); |
| bool platformSupportsDismissingBarrier; |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| platformSupportsDismissingBarrier = false; |
| break; |
| case TargetPlatform.iOS: |
| platformSupportsDismissingBarrier = true; |
| break; |
| } |
| assert(platformSupportsDismissingBarrier != null); |
| final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier; |
| final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible; |
| 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( |
| onAnyTapDown: () { |
| if (dismissible) |
| Navigator.maybePop(context); |
| }, |
| child: Semantics( |
| label: semanticsDismissible ? semanticsLabel : null, |
| textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, |
| child: ConstrainedBox( |
| constraints: const BoxConstraints.expand(), |
| child: color == null ? null : DecoratedBox( |
| decoration: BoxDecoration( |
| 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({ |
| Key key, |
| Animation<Color> color, |
| this.dismissible = true, |
| this.semanticsLabel, |
| this.barrierSemanticsDismissible, |
| }) : super(key: key, 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; |
| |
| /// 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; |
| |
| @override |
| Widget build(BuildContext context) { |
| return ModalBarrier( |
| color: color?.value, |
| dismissible: dismissible, |
| semanticsLabel: semanticsLabel, |
| barrierSemanticsDismissible: barrierSemanticsDismissible, |
| ); |
| } |
| } |
| |
| // 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 PrimaryPointerGestureRecognizer { |
| _AnyTapGestureRecognizer({ |
| Object debugOwner, |
| }) : super(debugOwner: debugOwner); |
| |
| VoidCallback onAnyTapDown; |
| |
| bool _sentTapDown = false; |
| bool _wonArenaForPrimaryPointer = false; |
| // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a |
| // different set of buttons, the gesture is canceled. |
| int _initialButtons; |
| |
| @override |
| void addAllowedPointer(PointerDownEvent event) { |
| super.addAllowedPointer(event); |
| // `_initialButtons` must be assigned here instead of `handlePrimaryPointer`, |
| // because `acceptGesture` might be called before `handlePrimaryPointer`, |
| // which relies on `_initialButtons` to create `TapDownDetails`. |
| _initialButtons = event.buttons; |
| } |
| |
| @override |
| void handlePrimaryPointer(PointerEvent event) { |
| if (event is PointerUpEvent || event is PointerCancelEvent) { |
| resolve(GestureDisposition.rejected); |
| _reset(); |
| } else if (event.buttons != _initialButtons) { |
| resolve(GestureDisposition.rejected); |
| stopTrackingPointer(primaryPointer); |
| } |
| } |
| |
| @override |
| void resolve(GestureDisposition disposition) { |
| if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) { |
| // This can happen if the gesture has been canceled. For example, when |
| // the pointer has exceeded the touch slop, the buttons have been changed, |
| // or if the recognizer is disposed. |
| assert(_sentTapDown); |
| _reset(); |
| } |
| super.resolve(disposition); |
| } |
| |
| @override |
| void didExceedDeadlineWithEvent(PointerDownEvent event) { |
| _checkDown(event.pointer); |
| } |
| |
| @override |
| void acceptGesture(int pointer) { |
| super.acceptGesture(pointer); |
| if (pointer == primaryPointer) { |
| _checkDown(pointer); |
| _wonArenaForPrimaryPointer = true; |
| _reset(); |
| } |
| } |
| |
| @override |
| void rejectGesture(int pointer) { |
| super.rejectGesture(pointer); |
| if (pointer == primaryPointer) { |
| // Another gesture won the arena. |
| assert(state != GestureRecognizerState.possible); |
| _reset(); |
| } |
| } |
| |
| void _checkDown(int pointer) { |
| if (_sentTapDown) |
| return; |
| if (onAnyTapDown != null) |
| onAnyTapDown(); |
| _sentTapDown = true; |
| } |
| |
| void _reset() { |
| _sentTapDown = false; |
| _wonArenaForPrimaryPointer = false; |
| _initialButtons = null; |
| } |
| |
| @override |
| String get debugDescription => 'any tap'; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena')); |
| properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down')); |
| } |
| } |
| |
| class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate { |
| const _ModalBarrierSemanticsDelegate({this.onAnyTapDown}); |
| |
| final VoidCallback onAnyTapDown; |
| |
| @override |
| void assignSemantics(RenderSemanticsGestureHandler renderObject) { |
| renderObject.onTap = onAnyTapDown; |
| } |
| } |
| |
| |
| class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> { |
| const _AnyTapGestureRecognizerFactory({this.onAnyTapDown}); |
| |
| final VoidCallback onAnyTapDown; |
| |
| @override |
| _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer(); |
| |
| @override |
| void initializer(_AnyTapGestureRecognizer instance) { |
| instance.onAnyTapDown = onAnyTapDown; |
| } |
| } |
| |
| // A GestureDetector used by ModalBarrier. It only has one callback, |
| // [onAnyTapDown], which recognizes tap down unconditionally. |
| class _ModalBarrierGestureDetector extends StatelessWidget { |
| const _ModalBarrierGestureDetector({ |
| Key key, |
| @required this.child, |
| @required this.onAnyTapDown, |
| }) : assert(child != null), |
| assert(onAnyTapDown != null), |
| super(key: key); |
| |
| /// The widget below this widget in the tree. |
| /// See [RawGestureDetector.child]. |
| final Widget child; |
| |
| /// Immediately called when a pointer causes a tap down. |
| /// See [_AnyTapGestureRecognizer.onAnyTapDown]. |
| final VoidCallback onAnyTapDown; |
| |
| @override |
| Widget build(BuildContext context) { |
| final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{ |
| _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapDown: onAnyTapDown), |
| }; |
| |
| return RawGestureDetector( |
| gestures: gestures, |
| behavior: HitTestBehavior.opaque, |
| semantics: _ModalBarrierSemanticsDelegate(onAnyTapDown: onAnyTapDown), |
| child: child, |
| ); |
| } |
| } |