modal_barrier.dart 9.21 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4
// 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.

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 82 83 84 85 86 87 88 89
    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;
90
    final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
91 92
    return BlockSemantics(
      child: ExcludeSemantics(
93 94 95
        // 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,
96 97
        child: _ModalBarrierGestureDetector(
          onAnyTapDown: () {
98
            if (dismissible)
99
              Navigator.maybePop(context);
100
          },
101
          child: Semantics(
102 103
            label: semanticsDismissible ? semanticsLabel : null,
            textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
104
            child: ConstrainedBox(
105
              constraints: const BoxConstraints.expand(),
106 107
              child: color == null ? null : DecoratedBox(
                decoration: BoxDecoration(
108
                  color: color,
109 110 111 112 113 114
                ),
              ),
            ),
          ),
        ),
      ),
115 116 117 118
    );
  }
}

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

145
  /// If non-null, fill the barrier with this color.
146 147 148 149 150
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
151
  Animation<Color> get color => listenable;
152 153

  /// Whether touching the barrier will pop the current route off the [Navigator].
154 155 156 157 158
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
159
  final bool dismissible;
160

161
  /// Semantics label used for the barrier if it is [dismissible].
162 163 164 165 166 167 168 169 170
  ///
  /// 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;

171 172 173
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
174
  ///
175 176 177 178
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
  final bool barrierSemanticsDismissible;

179
  @override
180
  Widget build(BuildContext context) {
181
    return ModalBarrier(
182
      color: color?.value,
183
      dismissible: dismissible,
184
      semanticsLabel: semanticsLabel,
185
      barrierSemanticsDismissible: barrierSemanticsDismissible,
186 187 188
    );
  }
}
189 190 191 192 193

// 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.
194 195 196
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
  _AnyTapGestureRecognizer({ Object debugOwner })
    : super(debugOwner: debugOwner);
197 198 199

  VoidCallback onAnyTapDown;

200
  @protected
201
  @override
202 203 204 205
  bool isPointerAllowed(PointerDownEvent event) {
    if (onAnyTapDown == null)
      return false;
    return super.isPointerAllowed(event);
206 207
  }

208
  @protected
209
  @override
210 211 212
  void handleTapDown({PointerDownEvent down}) {
    if (onAnyTapDown != null)
      onAnyTapDown();
213 214
  }

215
  @protected
216
  @override
217 218
  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
    // Do nothing.
219 220
  }

221
  @protected
222
  @override
223 224
  void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
    // Do nothing.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
  }

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

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,
    );
  }
}