modal_barrier.dart 14.7 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 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
/// A widget that modifies the size of the [SemanticsNode.rect] created by its
/// child widget.
///
/// It clips the focus in potentially four directions based on the
/// specified [EdgeInsets].
///
/// The size of the accessibility focus is adjusted based on value changes
/// inside the given [ValueNotifier].
///
/// See also:
///
///  * [ModalBarrier], which utilizes this widget to adjust the barrier focus
/// size based on the size of the content layer rendered on top of it.
class _SemanticsClipper extends SingleChildRenderObjectWidget{
  /// creates a [SemanticsClipper] that updates the size of the
  /// [SemanticsNode.rect] of its child based on the value inside the provided
  /// [ValueNotifier], or a default value of [EdgeInsets.zero].
  const _SemanticsClipper({
    super.child,
    required this.clipDetailsNotifier,
  });

  /// The [ValueNotifier] whose value determines how the child's
  /// [SemanticsNode.rect] should be clipped in four directions.
  final ValueNotifier<EdgeInsets> clipDetailsNotifier;

  @override
  _RenderSemanticsClipper createRenderObject(BuildContext context) {
    return _RenderSemanticsClipper(clipDetailsNotifier: clipDetailsNotifier,);
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSemanticsClipper renderObject) {
    renderObject.clipDetailsNotifier = clipDetailsNotifier;
  }
}
/// Updates the [SemanticsNode.rect] of its child based on the value inside
/// provided [ValueNotifier].
class _RenderSemanticsClipper extends RenderProxyBox {
Lioness100's avatar
Lioness100 committed
56
  /// Creates a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  /// based on the value inside provided [ValueNotifier].
  _RenderSemanticsClipper({
    required ValueNotifier<EdgeInsets> clipDetailsNotifier,
    RenderBox? child,
  }) : _clipDetailsNotifier = clipDetailsNotifier,
      super(child);

  ValueNotifier<EdgeInsets> _clipDetailsNotifier;

  /// The getter and setter retrieves / updates the [ValueNotifier] associated
  /// with this clipper.
  ValueNotifier<EdgeInsets> get clipDetailsNotifier => _clipDetailsNotifier;
  set clipDetailsNotifier (ValueNotifier<EdgeInsets> newNotifier) {
    if (_clipDetailsNotifier == newNotifier) {
      return;
    }
    if(attached) {
      _clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate);
    }
    _clipDetailsNotifier = newNotifier;
    _clipDetailsNotifier.addListener(markNeedsSemanticsUpdate);
    markNeedsSemanticsUpdate();
  }

  @override
  Rect get semanticBounds {
83
    final EdgeInsets clipDetails = _clipDetailsNotifier.value;
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    final Rect originalRect = super.semanticBounds;
    final Rect clippedRect = Rect.fromLTRB(
      originalRect.left + clipDetails.left,
      originalRect.top + clipDetails.top,
      originalRect.right - clipDetails.right,
      originalRect.bottom - clipDetails.bottom,
    );
    return clippedRect;
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    clipDetailsNotifier.addListener(markNeedsSemanticsUpdate);
  }

  @override
  void detach() {
    clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate);
    super.detach();
  }

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isSemanticBoundary = true;
  }
}

113
/// A widget that prevents the user from interacting with widgets behind itself.
114 115 116 117 118 119 120 121 122 123 124 125 126
///
/// 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.
127
class ModalBarrier extends StatelessWidget {
128
  /// Creates a widget that blocks user interaction.
129
  const ModalBarrier({
130
    super.key,
Hixie's avatar
Hixie committed
131
    this.color,
132
    this.dismissible = true,
133
    this.onDismiss,
134
    this.semanticsLabel,
135
    this.barrierSemanticsDismissible = true,
136 137
    this.clipDetailsNotifier,
    this.semanticsOnTapHint,
138
  });
139

140
  /// If non-null, fill the barrier with this color.
141 142 143 144 145
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
146
  final Color? color;
147

148
  /// Specifies if the barrier will be dismissed when the user taps on it.
149 150 151 152 153
  ///
  /// 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.
154 155 156 157 158
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [ModalBarrier] built by [ModalRoute] pages.
159
  final bool dismissible;
Adam Barth's avatar
Adam Barth committed
160

161
  /// {@template flutter.widgets.ModalBarrier.onDismiss}
162 163 164 165 166 167 168 169
  /// 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.
170
  /// {@endtemplate}
171 172
  final VoidCallback? onDismiss;

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

181
  /// Semantics label used for the barrier if it is [dismissible].
182 183 184 185 186 187 188 189
  ///
  /// 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.
190
  final String? semanticsLabel;
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205
  /// {@template flutter.widgets.ModalBarrier.clipDetailsNotifier}
  /// Contains a value of type [EdgeInsets] that specifies how the
  /// [SemanticsNode.rect] of the widget should be clipped.
  ///
  /// See also:
  ///
  ///  * [_SemanticsClipper], which utilizes the value inside to update the
  /// [SemanticsNode.rect] for its child.
  /// {@endtemplate}
  final ValueNotifier<EdgeInsets>? clipDetailsNotifier;

  /// {@macro flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
  final String? semanticsOnTapHint;

206
  @override
Adam Barth's avatar
Adam Barth committed
207
  Widget build(BuildContext context) {
208
    assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
209
    final bool platformSupportsDismissingBarrier;
210 211
    switch (defaultTargetPlatform) {
      case TargetPlatform.fuchsia:
212 213
      case TargetPlatform.linux:
      case TargetPlatform.windows:
214 215
        platformSupportsDismissingBarrier = false;
        break;
216
      case TargetPlatform.android:
217
      case TargetPlatform.iOS:
218
      case TargetPlatform.macOS:
219 220 221 222
        platformSupportsDismissingBarrier = true;
        break;
    }
    final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier;
223
    final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
224 225

    void handleDismiss() {
226 227 228 229 230 231 232 233 234
      if (dismissible) {
        if (onDismiss != null) {
          onDismiss!();
        } else {
          Navigator.maybePop(context);
        }
      } else {
        SystemSound.play(SystemSoundType.alert);
      }
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
    Widget barrier = Semantics(
      onTapHint: semanticsOnTapHint,
      onTap: semanticsDismissible && semanticsLabel != null ? handleDismiss : null,
      onDismiss: semanticsDismissible && semanticsLabel != null ? handleDismiss : null,
      label: semanticsDismissible ? semanticsLabel : null,
      textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
      child: MouseRegion(
        cursor: SystemMouseCursors.basic,
        child: ConstrainedBox(
        constraints: const BoxConstraints.expand(),
        child: color == null ? null : ColoredBox(
          color: color!,
          ),
        ),
      ),
    );

    // Developers can set [dismissible: true] and [barrierSemanticsDismissible: true]
    // to allow assistive technology users to dismiss a modal BottomSheet by
    // tapping on the Scrim focus.
    // On iOS, some modal barriers are not dismissible in accessibility mode.
    final bool excluding = !semanticsDismissible || !modalBarrierSemanticsDismissible;

    if (!excluding && clipDetailsNotifier != null) {
      barrier = _SemanticsClipper(
        clipDetailsNotifier: clipDetailsNotifier!,
        child: barrier,
      );
    }

267 268
    return BlockSemantics(
      child: ExcludeSemantics(
269
        excluding: excluding,
270
        child: _ModalBarrierGestureDetector(
271
          onDismiss: handleDismiss,
272
          child: barrier,
273 274
        ),
      ),
275 276 277 278
    );
  }
}

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
/// 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.
295
class AnimatedModalBarrier extends AnimatedWidget {
296
  /// Creates a widget that blocks user interaction.
297
  const AnimatedModalBarrier({
298
    super.key,
299
    required Animation<Color?> color,
300
    this.dismissible = true,
301
    this.semanticsLabel,
302
    this.barrierSemanticsDismissible,
303
    this.onDismiss,
304 305
    this.clipDetailsNotifier,
    this.semanticsOnTapHint,
306
  }) : super(listenable: color);
307

308
  /// If non-null, fill the barrier with this color.
309 310 311 312 313
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierColor], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
314
  Animation<Color?> get color => listenable as Animation<Color?>;
315 316

  /// Whether touching the barrier will pop the current route off the [Navigator].
317 318 319 320 321
  ///
  /// See also:
  ///
  ///  * [ModalRoute.barrierDismissible], which controls this property for the
  ///    [AnimatedModalBarrier] built by [ModalRoute] pages.
322
  final bool dismissible;
323

324
  /// Semantics label used for the barrier if it is [dismissible].
325 326 327 328 329 330 331
  ///
  /// 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.
332
  final String? semanticsLabel;
333

334 335 336
  /// Whether the modal barrier semantics are included in the semantics tree.
  ///
  /// See also:
337
  ///
338 339
  ///  * [ModalRoute.semanticsDismissible], which controls this property for
  ///    the [ModalBarrier] built by [ModalRoute] pages.
340
  final bool? barrierSemanticsDismissible;
341

342 343 344
  /// {@macro flutter.widgets.ModalBarrier.onDismiss}
  final VoidCallback? onDismiss;

345 346 347 348 349 350 351 352 353 354 355 356 357
  /// {@macro flutter.widgets.ModalBarrier.clipDetailsNotifier}
  final ValueNotifier<EdgeInsets>? clipDetailsNotifier;

  /// This hint text instructs users what they are able to do when they tap on
  /// the [ModalBarrier]
  ///
  /// E.g. If the hint text is 'close bottom sheet", it will be announced as
  /// "Double tap to close bottom sheet".
  ///
  /// If this value is null, the default onTapHint will be applied, resulting
  /// in the announcement of 'Double tap to activate'.
  final String? semanticsOnTapHint;

358
  @override
359
  Widget build(BuildContext context) {
360
    return ModalBarrier(
361
      color: color.value,
362
      dismissible: dismissible,
363
      semanticsLabel: semanticsLabel,
364
      barrierSemanticsDismissible: barrierSemanticsDismissible,
365
      onDismiss: onDismiss,
366 367
      clipDetailsNotifier: clipDetailsNotifier,
      semanticsOnTapHint: semanticsOnTapHint,
368 369 370
    );
  }
}
371 372 373 374 375

// 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.
376
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
377
  _AnyTapGestureRecognizer();
378

379
  VoidCallback? onAnyTapUp;
380

381
  @protected
382
  @override
383
  bool isPointerAllowed(PointerDownEvent event) {
384
    if (onAnyTapUp == null) {
385
      return false;
386
    }
387
    return super.isPointerAllowed(event);
388 389
  }

390
  @protected
391
  @override
392
  void handleTapDown({PointerDownEvent? down}) {
393
    // Do nothing.
394 395
  }

396
  @protected
397
  @override
398
  void handleTapUp({PointerDownEvent? down, PointerUpEvent? up}) {
399
    onAnyTapUp?.call();
400 401
  }

402
  @protected
403
  @override
404
  void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) {
405
    // Do nothing.
406 407 408 409 410 411 412
  }

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

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

415
  final VoidCallback? onAnyTapUp;
416 417 418 419 420 421

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

  @override
  void initializer(_AnyTapGestureRecognizer instance) {
422
    instance.onAnyTapUp = onAnyTapUp;
423 424 425 426 427 428 429
  }
}

// A GestureDetector used by ModalBarrier. It only has one callback,
// [onAnyTapDown], which recognizes tap down unconditionally.
class _ModalBarrierGestureDetector extends StatelessWidget {
  const _ModalBarrierGestureDetector({
430 431
    required this.child,
    required this.onDismiss,
432
  });
433 434 435 436 437

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

438 439 440
  /// Immediately called when an event that should dismiss the modal barrier
  /// has happened.
  final VoidCallback onDismiss;
441 442 443 444

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
445
      _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss),
446 447 448 449 450 451 452 453 454
    };

    return RawGestureDetector(
      gestures: gestures,
      behavior: HitTestBehavior.opaque,
      child: child,
    );
  }
}