modal_barrier.dart 10.2 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
    Key? 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
  }) : super(key: key);

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 64 65 66 67 68 69 70 71 72
  /// 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.
  final VoidCallback? onDismiss;

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

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

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

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

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

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

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

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

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

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

211
  @override
212
  Widget build(BuildContext context) {
213
    return ModalBarrier(
214
      color: color.value,
215
      dismissible: dismissible,
216
      semanticsLabel: semanticsLabel,
217
      barrierSemanticsDismissible: barrierSemanticsDismissible,
218 219 220
    );
  }
}
221 222 223 224 225

// 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.
226
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
227
  _AnyTapGestureRecognizer({ Object? debugOwner })
228
    : super(debugOwner: debugOwner);
229

230
  VoidCallback? onAnyTapUp;
231

232
  @protected
233
  @override
234
  bool isPointerAllowed(PointerDownEvent event) {
235
    if (onAnyTapUp == null)
236 237
      return false;
    return super.isPointerAllowed(event);
238 239
  }

240
  @protected
241
  @override
242
  void handleTapDown({PointerDownEvent? down}) {
243
    // Do nothing.
244 245
  }

246
  @protected
247
  @override
248
  void handleTapUp({PointerDownEvent? down, PointerUpEvent? up}) {
249
    onAnyTapUp?.call();
250 251
  }

252
  @protected
253
  @override
254
  void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) {
255
    // Do nothing.
256 257 258 259 260 261 262
  }

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

class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
263
  const _ModalBarrierSemanticsDelegate({this.onDismiss});
264

265
  final VoidCallback? onDismiss;
266 267 268

  @override
  void assignSemantics(RenderSemanticsGestureHandler renderObject) {
269
    renderObject.onTap = onDismiss;
270 271 272 273 274
  }
}


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

277
  final VoidCallback? onAnyTapUp;
278 279 280 281 282 283

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

  @override
  void initializer(_AnyTapGestureRecognizer instance) {
284
    instance.onAnyTapUp = onAnyTapUp;
285 286 287 288 289 290 291
  }
}

// A GestureDetector used by ModalBarrier. It only has one callback,
// [onAnyTapDown], which recognizes tap down unconditionally.
class _ModalBarrierGestureDetector extends StatelessWidget {
  const _ModalBarrierGestureDetector({
292 293 294
    Key? key,
    required this.child,
    required this.onDismiss,
295
  }) : assert(child != null),
296
       assert(onDismiss != null),
297 298 299 300 301 302
       super(key: key);

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

303 304 305
  /// Immediately called when an event that should dismiss the modal barrier
  /// has happened.
  final VoidCallback onDismiss;
306 307 308 309

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
310
      _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss),
311 312 313 314 315
    };

    return RawGestureDetector(
      gestures: gestures,
      behavior: HitTestBehavior.opaque,
316
      semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss),
317 318 319 320
      child: child,
    );
  }
}