bottom_sheet.dart 27.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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 'dart:ui' show lerpDouble;
6

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/widgets.dart';

10
import 'bottom_sheet_theme.dart';
11
import 'colors.dart';
12
import 'curves.dart';
13
import 'debug.dart';
14
import 'material.dart';
15
import 'material_localizations.dart';
16
import 'scaffold.dart';
17
import 'theme.dart';
18

19 20 21
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
const Curve _modalBottomSheetCurve = decelerateEasing;
22 23
const double _minFlingVelocity = 700.0;
const double _closeProgressThreshold = 0.5;
24

25 26 27
/// A callback for when the user begins dragging the bottom sheet.
///
/// Used by [BottomSheet.onDragStart].
28
typedef BottomSheetDragStartHandler = void Function(DragStartDetails details);
29 30 31 32

/// A callback for when the user stops dragging the bottom sheet.
///
/// Used by [BottomSheet.onDragEnd].
33 34
typedef BottomSheetDragEndHandler = void Function(
  DragEndDetails details, {
35
  required bool isClosing,
36 37
});

38 39 40 41 42 43 44 45
/// A material design bottom sheet.
///
/// There are two kinds of bottom sheets in material design:
///
///  * _Persistent_. A persistent bottom sheet shows information that
///    supplements the primary content of the app. A persistent bottom sheet
///    remains visible even when the user interacts with other parts of the app.
///    Persistent bottom sheets can be created and displayed with the
46 47
///    [ScaffoldState.showBottomSheet] function or by specifying the
///    [Scaffold.bottomSheet] constructor parameter.
48 49 50 51 52 53 54
///
///  * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
///    prevents the user from interacting with the rest of the app. Modal bottom
///    sheets can be created and displayed with the [showModalBottomSheet]
///    function.
///
/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
55 56
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
57 58 59
///
/// See also:
///
60 61 62 63 64
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal "persistent" bottom sheets.
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * <https://material.io/design/components/sheets-bottom.html>
65
class BottomSheet extends StatefulWidget {
66 67 68
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
69
  /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
70
  /// [showModalBottomSheet], for modal bottom sheets.
71
  const BottomSheet({
72
    Key? key,
73
    this.animationController,
74
    this.enableDrag = true,
75 76
    this.onDragStart,
    this.onDragEnd,
77
    this.backgroundColor,
78 79
    this.elevation,
    this.shape,
80
    this.clipBehavior,
81
    this.constraints,
82 83
    required this.onClosing,
    required this.builder,
84 85
  }) : assert(enableDrag != null),
       assert(onClosing != null),
86
       assert(builder != null),
87
       assert(elevation == null || elevation >= 0.0),
88
       super(key: key);
89

90 91
  /// The animation controller that controls the bottom sheet's entrance and
  /// exit animations.
92 93 94
  ///
  /// The BottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
95
  final AnimationController? animationController;
96 97 98

  /// Called when the bottom sheet begins to close.
  ///
99
  /// A bottom sheet might be prevented from closing (e.g., by user
100 101
  /// interaction) even after this callback is called. For this reason, this
  /// callback might be call multiple times for a given bottom sheet.
102
  final VoidCallback onClosing;
103 104 105 106 107

  /// A builder for the contents of the sheet.
  ///
  /// The bottom sheet will wrap the widget produced by this builder in a
  /// [Material] widget.
108 109
  final WidgetBuilder builder;

110
  /// If true, the bottom sheet can be dragged up and down and dismissed by
111
  /// swiping downwards.
112 113 114 115
  ///
  /// Default is true.
  final bool enableDrag;

116 117 118 119 120
  /// Called when the user begins dragging the bottom sheet vertically, if
  /// [enableDrag] is true.
  ///
  /// Would typically be used to change the bottom sheet animation curve so
  /// that it tracks the user's finger accurately.
121
  final BottomSheetDragStartHandler? onDragStart;
122 123 124 125 126 127 128

  /// Called when the user stops dragging the bottom sheet, if [enableDrag]
  /// is true.
  ///
  /// Would typically be used to reset the bottom sheet animation curve, so
  /// that it animates non-linearly. Called before [onClosing] if the bottom
  /// sheet is closing.
129
  final BottomSheetDragEndHandler? onDragEnd;
130

131 132 133 134 135
  /// The bottom sheet's background color.
  ///
  /// Defines the bottom sheet's [Material.color].
  ///
  /// Defaults to null and falls back to [Material]'s default.
136
  final Color? backgroundColor;
137

138
  /// The z-coordinate at which to place this material relative to its parent.
139
  ///
140 141 142
  /// This controls the size of the shadow below the material.
  ///
  /// Defaults to 0. The value is non-negative.
143
  final double? elevation;
144

145
  /// The shape of the bottom sheet.
146
  ///
147 148 149
  /// Defines the bottom sheet's [Material.shape].
  ///
  /// Defaults to null and falls back to [Material]'s default.
150
  final ShapeBorder? shape;
151

152
  /// {@macro flutter.material.Material.clipBehavior}
153 154 155 156 157 158 159 160
  ///
  /// Defines the bottom sheet's [Material.clipBehavior].
  ///
  /// Use this property to enable clipping of content when the bottom sheet has
  /// a custom [shape] and the content can extend past this shape. For example,
  /// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
  /// top.
  ///
161
  /// If this property is null then [BottomSheetThemeData.clipBehavior] of
162 163
  /// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior
  /// will be [Clip.none].
164
  final Clip? clipBehavior;
165

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
  /// Defines minimum and maximum sizes for a [BottomSheet].
  ///
  /// Typically a bottom sheet will cover the entire width of its
  /// parent. However for large screens you may want to limit the width
  /// to something smaller and this property provides a way to specify
  /// a maximum width.
  ///
  /// If null, then the ambient [ThemeData.bottomSheetTheme]'s
  /// [BottomSheetThemeData.constraints] will be used. If that
  /// is null then the bottom sheet's size will be constrained
  /// by its parent (usually a [Scaffold]).
  ///
  /// If constraints are specified (either in this property or in the
  /// theme), the bottom sheet will be aligned to the bottom-center of
  /// the available space. Otherwise, no alignment is applied.
  final BoxConstraints? constraints;

183
  @override
184
  State<BottomSheet> createState() => _BottomSheetState();
185

186 187 188 189 190 191
  /// Creates an [AnimationController] suitable for a
  /// [BottomSheet.animationController].
  ///
  /// This API available as a convenience for a Material compliant bottom sheet
  /// animation. If alternative animation durations are required, a different
  /// animation controller could be provided.
192
  static AnimationController createAnimationController(TickerProvider vsync) {
193
    return AnimationController(
194 195
      duration: _bottomSheetEnterDuration,
      reverseDuration: _bottomSheetExitDuration,
196 197
      debugLabel: 'BottomSheet',
      vsync: vsync,
198 199
    );
  }
200 201 202 203
}

class _BottomSheetState extends State<BottomSheet> {

204
  final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
205 206

  double get _childHeight {
207
    final RenderBox renderBox = _childKey.currentContext!.findRenderObject()! as RenderBox;
208 209
    return renderBox.size.height;
  }
210

211
  bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse;
212

213
  void _handleDragStart(DragStartDetails details) {
214
    widget.onDragStart?.call(details);
215 216
  }

217
  void _handleDragUpdate(DragUpdateDetails details) {
218 219 220 221 222
    assert(
      widget.enableDrag && widget.animationController != null,
      "'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. "
      "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
    );
223 224
    if (_dismissUnderway)
      return;
225
    widget.animationController!.value -= details.primaryDelta! / _childHeight;
226 227
  }

228
  void _handleDragEnd(DragEndDetails details) {
229 230 231 232 233
    assert(
      widget.enableDrag && widget.animationController != null,
      "'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. "
      "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
    );
234 235
    if (_dismissUnderway)
      return;
236
    bool isClosing = false;
237
    if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
238
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
239 240
      if (widget.animationController!.value > 0.0) {
        widget.animationController!.fling(velocity: flingVelocity);
241 242
      }
      if (flingVelocity < 0.0) {
243
        isClosing = true;
244
      }
245 246 247
    } else if (widget.animationController!.value < _closeProgressThreshold) {
      if (widget.animationController!.value > 0.0)
        widget.animationController!.fling(velocity: -1.0);
248
      isClosing = true;
249
    } else {
250
      widget.animationController!.forward();
251 252
    }

253 254 255 256
    widget.onDragEnd?.call(
      details,
      isClosing: isClosing,
    );
257 258 259 260

    if (isClosing) {
      widget.onClosing();
    }
261 262 263 264 265
  }

  bool extentChanged(DraggableScrollableNotification notification) {
    if (notification.extent == notification.minExtent) {
      widget.onClosing();
266
    }
267
    return false;
268 269
  }

270
  @override
271
  Widget build(BuildContext context) {
272
    final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
273
    final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints;
274
    final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
275
    final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? 0;
276
    final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape;
277
    final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
278

279
    Widget bottomSheet = Material(
280
      key: _childKey,
281 282 283
      color: color,
      elevation: elevation,
      shape: shape,
284
      clipBehavior: clipBehavior,
285 286 287 288
      child: NotificationListener<DraggableScrollableNotification>(
        onNotification: extentChanged,
        child: widget.builder(context),
      ),
289
    );
290 291 292 293

    if (constraints != null) {
      bottomSheet = Align(
        alignment: Alignment.bottomCenter,
294
        heightFactor: 1.0,
295 296 297 298 299 300 301
        child: ConstrainedBox(
          constraints: constraints,
          child: bottomSheet,
        ),
      );
    }

302
    return !widget.enableDrag ? bottomSheet : GestureDetector(
303
      onVerticalDragStart: _handleDragStart,
304
      onVerticalDragUpdate: _handleDragUpdate,
305
      onVerticalDragEnd: _handleDragEnd,
306
      excludeFromSemantics: true,
307
      child: bottomSheet,
308 309 310 311
    );
  }
}

312
// PERSISTENT BOTTOM SHEETS
313

314
// See scaffold.dart
315 316


317
// MODAL BOTTOM SHEETS
318
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
319
  _ModalBottomSheetLayout(this.progress, this.isScrollControlled);
320 321

  final double progress;
322
  final bool isScrollControlled;
323

324
  @override
325
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
326
    return BoxConstraints(
327 328
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
329 330 331
      maxHeight: isScrollControlled
        ? constraints.maxHeight
        : constraints.maxHeight * 9.0 / 16.0,
332 333 334
    );
  }

335
  @override
336
  Offset getPositionForChild(Size size, Size childSize) {
337
    return Offset(0.0, size.height - childSize.height * progress);
338 339
  }

340
  @override
341 342
  bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress;
343 344 345
  }
}

346
class _ModalBottomSheet<T> extends StatefulWidget {
347
  const _ModalBottomSheet({
348
    Key? key,
349
    this.route,
350 351 352
    this.backgroundColor,
    this.elevation,
    this.shape,
353
    this.clipBehavior,
354
    this.constraints,
355
    this.isScrollControlled = false,
356
    this.enableDrag = true,
357
  }) : assert(isScrollControlled != null),
358
       assert(enableDrag != null),
359
       super(key: key);
360

361
  final _ModalBottomSheetRoute<T>? route;
362
  final bool isScrollControlled;
363 364 365 366
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
367
  final BoxConstraints? constraints;
368
  final bool enableDrag;
369

370
  @override
371
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
372 373
}

374
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
375 376
  ParametricCurve<double> animationCurve = _modalBottomSheetCurve;

377
  String _getRouteLabel(MaterialLocalizations localizations) {
378
    switch (Theme.of(context).platform) {
379
      case TargetPlatform.iOS:
380
      case TargetPlatform.macOS:
381
        return '';
382 383
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
384 385
      case TargetPlatform.linux:
      case TargetPlatform.windows:
386
        return localizations.dialogLabel;
387
    }
388 389
  }

390 391 392 393 394
  void handleDragStart(DragStartDetails details) {
    // Allow the bottom sheet to track the user's finger accurately.
    animationCurve = Curves.linear;
  }

395
  void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
396 397
    // Allow the bottom sheet to animate smoothly from its current position.
    animationCurve = _BottomSheetSuspendedCurve(
398
      widget.route!.animation!.value,
399 400 401 402
      curve: _modalBottomSheetCurve,
    );
  }

403 404 405 406
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasMaterialLocalizations(context));
407
    final MediaQueryData mediaQuery = MediaQuery.of(context);
408
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
409
    final String routeLabel = _getRouteLabel(localizations);
410

411
    return AnimatedBuilder(
412
      animation: widget.route!.animation!,
413 414 415 416 417 418 419 420 421 422 423 424
      child: BottomSheet(
        animationController: widget.route!._animationController,
        onClosing: () {
          if (widget.route!.isCurrent) {
            Navigator.pop(context);
          }
        },
        builder: widget.route!.builder!,
        backgroundColor: widget.backgroundColor,
        elevation: widget.elevation,
        shape: widget.shape,
        clipBehavior: widget.clipBehavior,
425
        constraints: widget.constraints,
426 427 428
        enableDrag: widget.enableDrag,
        onDragStart: handleDragStart,
        onDragEnd: handleDragEnd,
429
      ),
430
      builder: (BuildContext context, Widget? child) {
431 432
        // Disable the initial animation when accessible navigation is on so
        // that the semantics are added to the tree at the correct time.
433
        final double animationValue = animationCurve.transform(
434
            mediaQuery.accessibleNavigation ? 1.0 : widget.route!.animation!.value,
435
        );
436 437 438 439 440 441 442 443
        return Semantics(
          scopesRoute: true,
          namesRoute: true,
          label: routeLabel,
          explicitChildNodes: true,
          child: ClipRect(
            child: CustomSingleChildLayout(
              delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
444
              child: child,
445
            ),
446 447 448
          ),
        );
      },
449 450 451 452
    );
  }
}

Hixie's avatar
Hixie committed
453 454
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
  _ModalBottomSheetRoute({
455
    this.builder,
456
    required this.capturedThemes,
457
    this.barrierLabel,
458
    this.backgroundColor,
459 460
    this.elevation,
    this.shape,
461
    this.clipBehavior,
462
    this.constraints,
463
    this.modalBarrierColor,
464
    this.isDismissible = true,
465
    this.enableDrag = true,
466 467
    required this.isScrollControlled,
    RouteSettings? settings,
468
    this.transitionAnimationController,
469
  }) : assert(isScrollControlled != null),
470
       assert(isDismissible != null),
471
       assert(enableDrag != null),
472
       super(settings: settings);
473

474
  final WidgetBuilder? builder;
475
  final CapturedThemes capturedThemes;
476
  final bool isScrollControlled;
477 478 479 480
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
481
  final BoxConstraints? constraints;
482
  final Color? modalBarrierColor;
483
  final bool isDismissible;
484
  final bool enableDrag;
485
  final AnimationController? transitionAnimationController;
486

487
  @override
488 489 490 491
  Duration get transitionDuration => _bottomSheetEnterDuration;

  @override
  Duration get reverseTransitionDuration => _bottomSheetExitDuration;
492 493

  @override
494
  bool get barrierDismissible => isDismissible;
495

496
  @override
497
  final String? barrierLabel;
498

499
  @override
500
  Color get barrierColor => modalBarrierColor ?? Colors.black54;
501

502
  AnimationController? _animationController;
503

504
  @override
505
  AnimationController createAnimationController() {
506
    assert(_animationController == null);
507 508 509 510 511 512
    if (transitionAnimationController != null) {
      _animationController = transitionAnimationController;
      willDisposeAnimationController = false;
    } else {
      _animationController = BottomSheet.createAnimationController(navigator!.overlay!);
    }
513
    return _animationController!;
514 515
  }

516
  @override
517
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
518 519
    // By definition, the bottom sheet is aligned to the bottom of the page
    // and isn't exposed to the top padding of the MediaQuery.
520
    final Widget bottomSheet = MediaQuery.removePadding(
521 522
      context: context,
      removeTop: true,
523 524
      child: Builder(
        builder: (BuildContext context) {
525
          final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
526 527 528 529 530 531
          return _ModalBottomSheet<T>(
            route: this,
            backgroundColor: backgroundColor ?? sheetTheme.modalBackgroundColor ?? sheetTheme.backgroundColor,
            elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
            shape: shape,
            clipBehavior: clipBehavior,
532
            constraints: constraints,
533 534 535 536
            isScrollControlled: isScrollControlled,
            enableDrag: enableDrag,
          );
        },
537
      ),
538
    );
539
    return capturedThemes.wrap(bottomSheet);
540 541 542
  }
}

543 544
// TODO(guidezpl): Look into making this public. A copy of this class is in
//  scaffold.dart, for now, https://github.com/flutter/flutter/issues/51627
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
/// A curve that progresses linearly until a specified [startingPoint], at which
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
/// but will use [startingPoint] as the Y position.
///
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
/// straight line, and the top-right quarter will contain the entire contents of
/// [Curves.easeOut].
///
/// This is useful in situations where a widget must track the user's finger
/// (which requires a linear animation), and afterwards can be flung using a
/// curve specified with the [curve] argument, after the finger is released. In
/// such a case, the value of [startingPoint] would be the progress of the
/// animation at the time when the finger was released.
///
/// The [startingPoint] and [curve] arguments must not be null.
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
  /// Creates a suspended curve.
  const _BottomSheetSuspendedCurve(
    this.startingPoint, {
    this.curve = Curves.easeOutCubic,
  }) : assert(startingPoint != null),
       assert(curve != null);

  /// The progress value at which [curve] should begin.
  ///
  /// This defaults to [Curves.easeOutCubic].
  final double startingPoint;

  /// The curve to use when [startingPoint] is reached.
  final Curve curve;

  @override
  double transform(double t) {
    assert(t >= 0.0 && t <= 1.0);
    assert(startingPoint >= 0.0 && startingPoint <= 1.0);

    if (t < startingPoint) {
      return t;
    }

    if (t == 1.0) {
      return t;
    }

    final double curveProgress = (t - startingPoint) / (1 - startingPoint);
    final double transformed = curve.transform(curveProgress);
592
    return lerpDouble(startingPoint, 1, transformed)!;
593 594 595 596 597 598 599 600
  }

  @override
  String toString() {
    return '${describeIdentity(this)}($startingPoint, $curve)';
  }
}

601 602 603 604 605 606 607
/// Shows a modal material design bottom sheet.
///
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
/// the user from interacting with the rest of the app.
///
/// A closely related widget is a persistent bottom sheet, which shows
/// information that supplements the primary content of the app without
608
/// preventing the user from interacting with the app. Persistent bottom sheets
609 610
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
611
///
Ian Hickson's avatar
Ian Hickson committed
612 613 614 615 616
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the bottom sheet. It is only used when the method is called. Its
/// corresponding widget can be safely removed from the tree before the bottom
/// sheet is closed.
///
617 618 619 620
/// The `isScrollControlled` parameter specifies whether this is a route for
/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish
/// to have a bottom sheet that has a scrollable child such as a [ListView] or
/// a [GridView] and have the bottom sheet be draggable, you should set this
621
/// parameter to true.
622
///
623 624 625 626 627
/// The `useRootNavigator` parameter ensures that the root navigator is used to
/// display the [BottomSheet] when set to `true`. This is useful in the case
/// that a modal [BottomSheet] needs to be displayed above all other content
/// but the caller is inside another [Navigator].
///
628 629 630
/// The [isDismissible] parameter specifies whether the bottom sheet will be
/// dismissed when user taps on the scrim.
///
631
/// The [enableDrag] parameter specifies whether the bottom sheet can be
632
/// dragged up and down and dismissed by swiping downwards.
633
///
634 635
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
636
/// parameters can be passed in to customize the appearance and behavior of
637 638
/// modal bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
639
///
640
/// The [transitionAnimationController] controls the bottom sheet's entrance and
641 642
/// exit animations. It's up to the owner of the controller to call
/// [AnimationController.dispose] when the controller is no longer needed.
643
///
644 645 646 647
/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet
/// sheet. This is particularly useful in the case that a user wants to observe
/// [PopupRoute]s within a [NavigatorObserver].
///
648
/// Returns a `Future` that resolves to the value (if any) that was passed to
649
/// [Navigator.pop] when the modal bottom sheet was closed.
650
///
651
/// {@tool dartpad}
652 653 654 655 656
/// This example demonstrates how to use `showModalBottomSheet` to display a
/// bottom sheet that obscures the content behind it when a user taps a button.
/// It also demonstrates how to close the bottom sheet using the [Navigator]
/// when a user taps on a button inside the bottom sheet.
///
657
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart **
658
/// {@end-tool}
659
///
660 661
/// See also:
///
662 663
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    function passed as the `builder` argument to [showModalBottomSheet].
664 665
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
666 667
///  * [DraggableScrollableSheet], which allows you to create a bottom sheet
///    that grows and then becomes scrollable once it reaches its maximum size.
668
///  * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet>
669
Future<T?> showModalBottomSheet<T>({
670 671 672 673 674 675
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
676
  BoxConstraints? constraints,
677
  Color? barrierColor,
678
  bool isScrollControlled = false,
679
  bool useRootNavigator = false,
680
  bool isDismissible = true,
681
  bool enableDrag = true,
682
  RouteSettings? routeSettings,
683
  AnimationController? transitionAnimationController,
684
}) {
685 686
  assert(context != null);
  assert(builder != null);
687
  assert(isScrollControlled != null);
688
  assert(useRootNavigator != null);
689
  assert(isDismissible != null);
690
  assert(enableDrag != null);
691
  assert(debugCheckHasMediaQuery(context));
692
  assert(debugCheckHasMaterialLocalizations(context));
693

694
  final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
695
  return navigator.push(_ModalBottomSheetRoute<T>(
696
    builder: builder,
697
    capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
698
    isScrollControlled: isScrollControlled,
699
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
700 701
    backgroundColor: backgroundColor,
    elevation: elevation,
702
    shape: shape,
703
    clipBehavior: clipBehavior,
704
    constraints: constraints,
705
    isDismissible: isDismissible,
706 707
    modalBarrierColor: barrierColor,
    enableDrag: enableDrag,
708
    settings: routeSettings,
709
    transitionAnimationController: transitionAnimationController,
710 711
  ));
}
712

713 714
/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If
/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet].
715
///
716 717 718
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
719 720
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
721
/// parameters can be passed in to customize the appearance and behavior of
722 723
/// persistent bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
724
///
725 726 727 728 729
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the controller returned by
/// this method.
///
/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
730
/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
731 732 733
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
734
/// does not add a back button to the enclosing Scaffold's app bar, use the
735 736
/// [Scaffold.bottomSheet] constructor parameter.
///
737 738 739 740 741 742 743 744 745 746 747
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
/// [showModalBottomSheet] function.
///
/// The `context` argument is used to look up the [Scaffold] for the bottom
/// sheet. It is only used when the method is called. Its corresponding widget
/// can be safely removed from the tree before the bottom sheet is closed.
///
/// See also:
///
748 749
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    `builder`.
750 751 752
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * [Scaffold.of], for information about how to obtain the [BuildContext].
753
///  * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
754
PersistentBottomSheetController<T> showBottomSheet<T>({
755 756 757 758 759 760
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
761
  BoxConstraints? constraints,
762
  AnimationController? transitionAnimationController,
763 764 765
}) {
  assert(context != null);
  assert(builder != null);
766 767
  assert(debugCheckHasScaffold(context));

768
  return Scaffold.of(context).showBottomSheet<T>(
769 770
    builder,
    backgroundColor: backgroundColor,
771 772
    elevation: elevation,
    shape: shape,
773
    clipBehavior: clipBehavior,
774
    constraints: constraints,
775
    transitionAnimationController: transitionAnimationController,
776
  );
777
}