modal_barrier.dart 9.36 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/gestures.dart';
7

Adam Barth's avatar
Adam Barth committed
8
import 'basic.dart';
9
import 'container.dart';
10
import 'debug.dart';
Adam Barth's avatar
Adam Barth committed
11
import 'framework.dart';
Hixie's avatar
Hixie committed
12
import 'gesture_detector.dart';
Adam Barth's avatar
Adam Barth committed
13
import 'navigator.dart';
14 15
import 'transitions.dart';

16
/// A widget that prevents the user from interacting with widgets behind itself.
17 18 19 20 21 22 23 24 25 26 27 28 29
///
/// 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.
30
class ModalBarrier extends StatelessWidget {
31
  /// Creates a widget that blocks user interaction.
32
  const ModalBarrier({
33
    Key key,
Hixie's avatar
Hixie committed
34
    this.color,
35
    this.dismissible = true,
36
    this.semanticsLabel,
37
    this.barrierSemanticsDismissible = true,
38 39
  }) : super(key: key);

40
  /// If non-null, fill the barrier with this color.
41 42 43 44 45
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
46
  final Color color;
47 48

  /// Whether touching the barrier will pop the current route off the [Navigator].
49 50 51 52 53
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
54
  final bool dismissible;
Adam Barth's avatar
Adam Barth committed
55

56 57 58
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
59
  ///
60 61 62 63
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
  final bool barrierSemanticsDismissible;

64
  /// Semantics label used for the barrier if it is [dismissible].
65 66 67 68 69 70 71 72 73 74
  ///
  /// 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;

75
  @override
Adam Barth's avatar
Adam Barth committed
76
  Widget build(BuildContext context) {
77
    assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
78 79 80 81
    bool platformSupportsDismissingBarrier;
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
82 83
      case TargetPlatform.linux:
      case TargetPlatform.windows:
84 85 86
        platformSupportsDismissingBarrier = false;
        break;
      case TargetPlatform.iOS:
87
      case TargetPlatform.macOS:
88 89 90 91 92
        platformSupportsDismissingBarrier = true;
        break;
    }
    assert(platformSupportsDismissingBarrier != null);
    final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier;
93
    final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
94 95
    return BlockSemantics(
      child: ExcludeSemantics(
96 97 98
        // 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,
99
        child: _ModalBarrierGestureDetector(
100
          onDismiss: () {
101
            if (dismissible)
102
              Navigator.maybePop(context);
103
          },
104
          child: Semantics(
105 106
            label: semanticsDismissible ? semanticsLabel : null,
            textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
107 108 109 110 111 112 113 114
            child: MouseRegion(
              opaque: true,
              child: ConstrainedBox(
                constraints: const BoxConstraints.expand(),
                child: color == null ? null : DecoratedBox(
                  decoration: BoxDecoration(
                    color: color,
                  ),
115 116 117 118 119 120
                ),
              ),
            ),
          ),
        ),
      ),
121 122 123 124
    );
  }
}

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
/// 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.
141
class AnimatedModalBarrier extends AnimatedWidget {
142
  /// Creates a widget that blocks user interaction.
143
  const AnimatedModalBarrier({
144
    Key key,
145
    Animation<Color> color,
146
    this.dismissible = true,
147
    this.semanticsLabel,
148
    this.barrierSemanticsDismissible,
149
  }) : super(key: key, listenable: color);
150

151
  /// If non-null, fill the barrier with this color.
152 153 154 155 156
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
157
  Animation<Color> get color => listenable as Animation<Color>;
158 159

  /// Whether touching the barrier will pop the current route off the [Navigator].
160 161 162 163 164
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
165
  final bool dismissible;
166

167
  /// Semantics label used for the barrier if it is [dismissible].
168 169 170 171 172 173 174 175 176
  ///
  /// 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;

177 178 179
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
180
  ///
181 182 183 184
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
  final bool barrierSemanticsDismissible;

185
  @override
186
  Widget build(BuildContext context) {
187
    return ModalBarrier(
188
      color: color?.value,
189
      dismissible: dismissible,
190
      semanticsLabel: semanticsLabel,
191
      barrierSemanticsDismissible: barrierSemanticsDismissible,
192 193 194
    );
  }
}
195 196 197 198 199

// 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.
200 201 202
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
  _AnyTapGestureRecognizer({ Object debugOwner })
    : super(debugOwner: debugOwner);
203

204
  VoidCallback onAnyTapUp;
205

206
  @protected
207
  @override
208
  bool isPointerAllowed(PointerDownEvent event) {
209
    if (onAnyTapUp == null)
210 211
      return false;
    return super.isPointerAllowed(event);
212 213
  }

214
  @protected
215
  @override
216
  void handleTapDown({PointerDownEvent down}) {
217
    // Do nothing.
218 219
  }

220
  @protected
221
  @override
222
  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
223 224
    if (onAnyTapUp != null)
      onAnyTapUp();
225 226
  }

227
  @protected
228
  @override
229 230
  void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
    // Do nothing.
231 232 233 234 235 236 237
  }

  @override
  String get debugDescription => 'any tap';
}

class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
238
  const _ModalBarrierSemanticsDelegate({this.onDismiss});
239

240
  final VoidCallback onDismiss;
241 242 243

  @override
  void assignSemantics(RenderSemanticsGestureHandler renderObject) {
244
    renderObject.onTap = onDismiss;
245 246 247 248 249
  }
}


class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
250
  const _AnyTapGestureRecognizerFactory({this.onAnyTapUp});
251

252
  final VoidCallback onAnyTapUp;
253 254 255 256 257 258

  @override
  _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer();

  @override
  void initializer(_AnyTapGestureRecognizer instance) {
259
    instance.onAnyTapUp = onAnyTapUp;
260 261 262 263 264 265 266 267 268
  }
}

// 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,
269
    @required this.onDismiss,
270
  }) : assert(child != null),
271
       assert(onDismiss != null),
272 273 274 275 276 277
       super(key: key);

  /// The widget below this widget in the tree.
  /// See [RawGestureDetector.child].
  final Widget child;

278 279 280
  /// Immediately called when an event that should dismiss the modal barrier
  /// has happened.
  final VoidCallback onDismiss;
281 282 283 284

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
285
      _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss),
286 287 288 289 290
    };

    return RawGestureDetector(
      gestures: gestures,
      behavior: HitTestBehavior.opaque,
291
      semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss),
292 293 294 295
      child: child,
    );
  }
}