modal_barrier.dart 10.3 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
import 'package:flutter/rendering.dart';
8
import 'package:flutter/services.dart';
9

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

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

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

50
  /// Specifies if the barrier will be dismissed when the user taps on it.
51 52 53 54 55
  ///
  /// 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.
56 57 58 59 60
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
61
  final bool dismissible;
Adam Barth's avatar
Adam Barth committed
62

63
  /// {@template flutter.widgets.ModalBarrier.onDismiss}
64 65 66 67 68 69 70 71
  /// 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.
72
  /// {@endtemplate}
73 74
  final VoidCallback? onDismiss;

75 76 77
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
78
  ///
79 80
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
81
  final bool? barrierSemanticsDismissible;
82

83
  /// Semantics label used for the barrier if it is [dismissible].
84 85 86 87 88 89 90 91
  ///
  /// 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.
92
  final String? semanticsLabel;
93

94
  @override
Adam Barth's avatar
Adam Barth committed
95
  Widget build(BuildContext context) {
96
    assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
97
    final bool platformSupportsDismissingBarrier;
98 99 100
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
101 102
      case TargetPlatform.linux:
      case TargetPlatform.windows:
103 104 105
        platformSupportsDismissingBarrier = false;
        break;
      case TargetPlatform.iOS:
106
      case TargetPlatform.macOS:
107 108 109 110 111
        platformSupportsDismissingBarrier = true;
        break;
    }
    assert(platformSupportsDismissingBarrier != null);
    final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier;
112
    final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
113 114

    void handleDismiss() {
115 116 117 118 119 120 121 122 123
      if (dismissible) {
        if (onDismiss != null) {
          onDismiss!();
        } else {
          Navigator.maybePop(context);
        }
      } else {
        SystemSound.play(SystemSoundType.alert);
      }
124 125
    }

126 127
    return BlockSemantics(
      child: ExcludeSemantics(
128 129 130
        // 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,
131
        child: _ModalBarrierGestureDetector(
132
          onDismiss: handleDismiss,
133
          child: Semantics(
134
            label: semanticsDismissible ? semanticsLabel : null,
135
            onDismiss: semanticsDismissible ? handleDismiss : null,
136
            textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
137
            child: MouseRegion(
138
              cursor: SystemMouseCursors.basic,
139 140
              child: ConstrainedBox(
                constraints: const BoxConstraints.expand(),
141 142
                child: color == null ? null : ColoredBox(
                  color: color!,
143 144 145 146 147 148
                ),
              ),
            ),
          ),
        ),
      ),
149 150 151 152
    );
  }
}

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
/// 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.
169
class AnimatedModalBarrier extends AnimatedWidget {
170
  /// Creates a widget that blocks user interaction.
171
  const AnimatedModalBarrier({
172
    super.key,
173
    required Animation<Color?> color,
174
    this.dismissible = true,
175
    this.semanticsLabel,
176
    this.barrierSemanticsDismissible,
177
    this.onDismiss,
178
  }) : super(listenable: color);
179

180
  /// If non-null, fill the barrier with this color.
181 182 183 184 185
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
186
  Animation<Color?> get color => listenable as Animation<Color?>;
187 188

  /// Whether touching the barrier will pop the current route off the [Navigator].
189 190 191 192 193
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
194
  final bool dismissible;
195

196
  /// Semantics label used for the barrier if it is [dismissible].
197 198 199 200 201 202 203
  ///
  /// 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.
204
  final String? semanticsLabel;
205

206 207 208
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
209
  ///
210 211
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
212
  final bool? barrierSemanticsDismissible;
213

214 215 216
  /// {@macro flutter.widgets.ModalBarrier.onDismiss}
  final VoidCallback? onDismiss;

217
  @override
218
  Widget build(BuildContext context) {
219
    return ModalBarrier(
220
      color: color.value,
221
      dismissible: dismissible,
222
      semanticsLabel: semanticsLabel,
223
      barrierSemanticsDismissible: barrierSemanticsDismissible,
224
      onDismiss: onDismiss,
225 226 227
    );
  }
}
228 229 230 231 232

// 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.
233
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
234
  _AnyTapGestureRecognizer();
235

236
  VoidCallback? onAnyTapUp;
237

238
  @protected
239
  @override
240
  bool isPointerAllowed(PointerDownEvent event) {
241
    if (onAnyTapUp == null) {
242
      return false;
243
    }
244
    return super.isPointerAllowed(event);
245 246
  }

247
  @protected
248
  @override
249
  void handleTapDown({PointerDownEvent? down}) {
250
    // Do nothing.
251 252
  }

253
  @protected
254
  @override
255
  void handleTapUp({PointerDownEvent? down, PointerUpEvent? up}) {
256
    onAnyTapUp?.call();
257 258
  }

259
  @protected
260
  @override
261
  void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) {
262
    // Do nothing.
263 264 265 266 267 268 269
  }

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

class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
270
  const _ModalBarrierSemanticsDelegate({this.onDismiss});
271

272
  final VoidCallback? onDismiss;
273 274 275

  @override
  void assignSemantics(RenderSemanticsGestureHandler renderObject) {
276
    renderObject.onTap = onDismiss;
277 278 279 280
  }
}

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

283
  final VoidCallback? onAnyTapUp;
284 285 286 287 288 289

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

  @override
  void initializer(_AnyTapGestureRecognizer instance) {
290
    instance.onAnyTapUp = onAnyTapUp;
291 292 293 294 295 296 297
  }
}

// A GestureDetector used by ModalBarrier. It only has one callback,
// [onAnyTapDown], which recognizes tap down unconditionally.
class _ModalBarrierGestureDetector extends StatelessWidget {
  const _ModalBarrierGestureDetector({
298 299
    required this.child,
    required this.onDismiss,
300
  }) : assert(child != null),
301
       assert(onDismiss != null);
302 303 304 305 306

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

307 308 309
  /// Immediately called when an event that should dismiss the modal barrier
  /// has happened.
  final VoidCallback onDismiss;
310 311 312 313

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
314
      _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss),
315 316 317 318 319
    };

    return RawGestureDetector(
      gestures: gestures,
      behavior: HitTestBehavior.opaque,
320
      semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss),
321 322 323 324
      child: child,
    );
  }
}