bottom_sheet.dart 25.5 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 6
// @dart = 2.8

7
import 'dart:ui' show lerpDouble;
8

9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/scheduler.dart';
11 12
import 'package:flutter/widgets.dart';

13
import 'bottom_sheet_theme.dart';
14
import 'colors.dart';
15
import 'curves.dart';
16
import 'debug.dart';
17
import 'material.dart';
18
import 'material_localizations.dart';
19
import 'scaffold.dart';
20
import 'theme.dart';
21

22 23 24
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
const Curve _modalBottomSheetCurve = decelerateEasing;
25 26
const double _minFlingVelocity = 700.0;
const double _closeProgressThreshold = 0.5;
27

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

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

41 42 43 44 45 46 47 48
/// 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
49 50
///    [ScaffoldState.showBottomSheet] function or by specifying the
///    [Scaffold.bottomSheet] constructor parameter.
51 52 53 54 55 56 57
///
///  * _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
58 59
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
60 61 62
///
/// See also:
///
63 64 65 66 67
///  * [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>
68
class BottomSheet extends StatefulWidget {
69 70 71
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
72
  /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
73
  /// [showModalBottomSheet], for modal bottom sheets.
74
  const BottomSheet({
75
    Key key,
76
    this.animationController,
77
    this.enableDrag = true,
78 79
    this.onDragStart,
    this.onDragEnd,
80
    this.backgroundColor,
81 82
    this.elevation,
    this.shape,
83
    this.clipBehavior,
84
    @required this.onClosing,
85
    @required this.builder,
86 87
  }) : assert(enableDrag != null),
       assert(onClosing != null),
88
       assert(builder != null),
89
       assert(elevation == null || elevation >= 0.0),
90
       super(key: key);
91

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

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

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

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

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
  /// 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.
  final BottomSheetDragStartHandler onDragStart;

  /// 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.
  final BottomSheetDragEndHandler onDragEnd;

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

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

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

154 155 156 157 158 159 160 161 162
  /// {@macro flutter.widgets.Clip}
  ///
  /// 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.
  ///
163
  /// If this property is null then [BottomSheetThemeData.clipBehavior] of
164 165
  /// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior
  /// will be [Clip.none].
166 167
  final Clip clipBehavior;

168
  @override
169
  _BottomSheetState createState() => _BottomSheetState();
170

171 172 173 174 175 176
  /// 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.
177
  static AnimationController createAnimationController(TickerProvider vsync) {
178
    return AnimationController(
179 180
      duration: _bottomSheetEnterDuration,
      reverseDuration: _bottomSheetExitDuration,
181 182
      debugLabel: 'BottomSheet',
      vsync: vsync,
183 184
    );
  }
185 186 187 188
}

class _BottomSheetState extends State<BottomSheet> {

189
  final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
190 191

  double get _childHeight {
192
    final RenderBox renderBox = _childKey.currentContext.findRenderObject() as RenderBox;
193 194
    return renderBox.size.height;
  }
195

196
  bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
197

198 199 200 201 202 203
  void _handleDragStart(DragStartDetails details) {
    if (widget.onDragStart != null) {
      widget.onDragStart(details);
    }
  }

204
  void _handleDragUpdate(DragUpdateDetails details) {
205
    assert(widget.enableDrag);
206 207
    if (_dismissUnderway)
      return;
208
    widget.animationController.value -= details.primaryDelta / (_childHeight ?? details.primaryDelta);
209 210
  }

211
  void _handleDragEnd(DragEndDetails details) {
212
    assert(widget.enableDrag);
213 214
    if (_dismissUnderway)
      return;
215
    bool isClosing = false;
216
    if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
217
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
218
      if (widget.animationController.value > 0.0) {
219
        widget.animationController.fling(velocity: flingVelocity);
220 221
      }
      if (flingVelocity < 0.0) {
222
        isClosing = true;
223 224
      }
    } else if (widget.animationController.value < _closeProgressThreshold) {
225 226
      if (widget.animationController.value > 0.0)
        widget.animationController.fling(velocity: -1.0);
227
      isClosing = true;
228
    } else {
229
      widget.animationController.forward();
230 231 232 233 234 235 236 237 238 239 240 241
    }

    if (widget.onDragEnd != null) {
      widget.onDragEnd(
        details,
        isClosing: isClosing,
      );
    }

    if (isClosing) {
      widget.onClosing();
    }
242 243 244 245 246
  }

  bool extentChanged(DraggableScrollableNotification notification) {
    if (notification.extent == notification.minExtent) {
      widget.onClosing();
247
    }
248
    return false;
249 250
  }

251
  @override
252
  Widget build(BuildContext context) {
253 254 255 256
    final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
    final Color color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
    final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? 0;
    final ShapeBorder shape = widget.shape ?? bottomSheetTheme.shape;
257
    final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
258

259
    final Widget bottomSheet = Material(
260
      key: _childKey,
261 262 263
      color: color,
      elevation: elevation,
      shape: shape,
264
      clipBehavior: clipBehavior,
265 266 267 268
      child: NotificationListener<DraggableScrollableNotification>(
        onNotification: extentChanged,
        child: widget.builder(context),
      ),
269
    );
270
    return !widget.enableDrag ? bottomSheet : GestureDetector(
271
      onVerticalDragStart: _handleDragStart,
272
      onVerticalDragUpdate: _handleDragUpdate,
273
      onVerticalDragEnd: _handleDragEnd,
274
      child: bottomSheet,
275
      excludeFromSemantics: true,
276 277 278 279
    );
  }
}

280
// PERSISTENT BOTTOM SHEETS
281

282
// See scaffold.dart
283 284


285
// MODAL BOTTOM SHEETS
286
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
287
  _ModalBottomSheetLayout(this.progress, this.isScrollControlled);
288 289

  final double progress;
290
  final bool isScrollControlled;
291

292
  @override
293
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
294
    return BoxConstraints(
295 296 297
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
      minHeight: 0.0,
298 299 300
      maxHeight: isScrollControlled
        ? constraints.maxHeight
        : constraints.maxHeight * 9.0 / 16.0,
301 302 303
    );
  }

304
  @override
305
  Offset getPositionForChild(Size size, Size childSize) {
306
    return Offset(0.0, size.height - childSize.height * progress);
307 308
  }

309
  @override
310 311
  bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress;
312 313 314
  }
}

315
class _ModalBottomSheet<T> extends StatefulWidget {
316 317 318
  const _ModalBottomSheet({
    Key key,
    this.route,
319 320 321
    this.backgroundColor,
    this.elevation,
    this.shape,
322
    this.clipBehavior,
323
    this.isScrollControlled = false,
324
    this.enableDrag = true,
325
  }) : assert(isScrollControlled != null),
326
       assert(enableDrag != null),
327
       super(key: key);
328

329
  final _ModalBottomSheetRoute<T> route;
330
  final bool isScrollControlled;
331 332 333
  final Color backgroundColor;
  final double elevation;
  final ShapeBorder shape;
334
  final Clip clipBehavior;
335
  final bool enableDrag;
336

337
  @override
338
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
339 340
}

341
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
342 343
  ParametricCurve<double> animationCurve = _modalBottomSheetCurve;

344
  String _getRouteLabel(MaterialLocalizations localizations) {
345
    switch (Theme.of(context).platform) {
346
      case TargetPlatform.iOS:
347
      case TargetPlatform.macOS:
348
        return '';
349 350
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
351 352
      case TargetPlatform.linux:
      case TargetPlatform.windows:
353
        return localizations.dialogLabel;
354
    }
355 356 357
    return null;
  }

358 359 360 361 362 363 364 365 366 367 368 369 370
  void handleDragStart(DragStartDetails details) {
    // Allow the bottom sheet to track the user's finger accurately.
    animationCurve = Curves.linear;
  }

  void handleDragEnd(DragEndDetails details, {bool isClosing}) {
    // Allow the bottom sheet to animate smoothly from its current position.
    animationCurve = _BottomSheetSuspendedCurve(
      widget.route.animation.value,
      curve: _modalBottomSheetCurve,
    );
  }

371 372 373 374 375 376 377
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasMaterialLocalizations(context));
    final MediaQueryData mediaQuery = MediaQuery.of(context);
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    final String routeLabel = _getRouteLabel(localizations);
378

379 380
    return AnimatedBuilder(
      animation: widget.route.animation,
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
      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,
        enableDrag: widget.enableDrag,
        onDragStart: handleDragStart,
        onDragEnd: handleDragEnd,
      ),
397 398 399
      builder: (BuildContext context, Widget child) {
        // Disable the initial animation when accessible navigation is on so
        // that the semantics are added to the tree at the correct time.
400 401 402
        final double animationValue = animationCurve.transform(
            mediaQuery.accessibleNavigation ? 1.0 : widget.route.animation.value
        );
403 404 405 406 407 408 409 410
        return Semantics(
          scopesRoute: true,
          namesRoute: true,
          label: routeLabel,
          explicitChildNodes: true,
          child: ClipRect(
            child: CustomSingleChildLayout(
              delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
411
              child: child,
412
            ),
413 414 415
          ),
        );
      },
416 417 418 419
    );
  }
}

Hixie's avatar
Hixie committed
420 421
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
  _ModalBottomSheetRoute({
422 423
    this.builder,
    this.theme,
424
    this.barrierLabel,
425
    this.backgroundColor,
426 427
    this.elevation,
    this.shape,
428
    this.clipBehavior,
429
    this.modalBarrierColor,
430
    this.isDismissible = true,
431
    this.enableDrag = true,
432
    @required this.isScrollControlled,
433
    RouteSettings settings,
434
  }) : assert(isScrollControlled != null),
435
       assert(isDismissible != null),
436
       assert(enableDrag != null),
437
       super(settings: settings);
438 439

  final WidgetBuilder builder;
440
  final ThemeData theme;
441 442
  final bool isScrollControlled;
  final Color backgroundColor;
443 444
  final double elevation;
  final ShapeBorder shape;
445
  final Clip clipBehavior;
446
  final Color modalBarrierColor;
447
  final bool isDismissible;
448
  final bool enableDrag;
449

450
  @override
451 452 453 454
  Duration get transitionDuration => _bottomSheetEnterDuration;

  @override
  Duration get reverseTransitionDuration => _bottomSheetExitDuration;
455 456

  @override
457
  bool get barrierDismissible => isDismissible;
458

459 460 461
  @override
  final String barrierLabel;

462
  @override
463
  Color get barrierColor => modalBarrierColor ?? Colors.black54;
464

465 466
  AnimationController _animationController;

467
  @override
468
  AnimationController createAnimationController() {
469
    assert(_animationController == null);
470
    _animationController = BottomSheet.createAnimationController(navigator.overlay);
471
    return _animationController;
472 473
  }

474
  @override
475
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
476
    final BottomSheetThemeData sheetTheme = theme?.bottomSheetTheme ?? Theme.of(context).bottomSheetTheme;
477 478
    // By definition, the bottom sheet is aligned to the bottom of the page
    // and isn't exposed to the top padding of the MediaQuery.
479
    Widget bottomSheet = MediaQuery.removePadding(
480 481
      context: context,
      removeTop: true,
482 483
      child: _ModalBottomSheet<T>(
        route: this,
484 485
        backgroundColor: backgroundColor ?? sheetTheme?.modalBackgroundColor ?? sheetTheme?.backgroundColor,
        elevation: elevation ?? sheetTheme?.modalElevation ?? sheetTheme?.elevation,
486
        shape: shape,
487
        clipBehavior: clipBehavior,
488
        isScrollControlled: isScrollControlled,
489
        enableDrag: enableDrag,
490
      ),
491
    );
492
    if (theme != null)
493
      bottomSheet = Theme(data: theme, child: bottomSheet);
494
    return bottomSheet;
495 496 497
  }
}

498 499
// 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
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
/// 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);
    return lerpDouble(startingPoint, 1, transformed);
  }

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

556 557 558 559 560 561 562 563
/// 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
/// preventing the use from interacting with the app. Persistent bottom sheets
564 565
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
566
///
Ian Hickson's avatar
Ian Hickson committed
567 568 569 570 571
/// 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.
///
572 573 574 575 576 577
/// 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
/// parameter to true.
///
578 579 580 581 582
/// 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].
///
583 584 585
/// The [isDismissible] parameter specifies whether the bottom sheet will be
/// dismissed when user taps on the scrim.
///
586
/// The [enableDrag] parameter specifies whether the bottom sheet can be
587
/// dragged up and down and dismissed by swiping downwards.
588
///
589 590 591 592
/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
/// parameters can be passed in to customize the appearance and behavior of
/// modal bottom sheets.
///
593 594 595 596
/// 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].
///
597
/// Returns a `Future` that resolves to the value (if any) that was passed to
598
/// [Navigator.pop] when the modal bottom sheet was closed.
599
///
600
/// {@tool dartpad --template=stateless_widget_scaffold}
601 602 603 604 605 606 607 608 609
///
/// 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.
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Center(
610
///     child: ElevatedButton(
611 612 613 614 615 616 617 618 619 620 621 622 623 624
///       child: const Text('showModalBottomSheet'),
///       onPressed: () {
///         showModalBottomSheet<void>(
///           context: context,
///           builder: (BuildContext context) {
///             return Container(
///               height: 200,
///               color: Colors.amber,
///               child: Center(
///                 child: Column(
///                   mainAxisAlignment: MainAxisAlignment.center,
///                   mainAxisSize: MainAxisSize.min,
///                   children: <Widget>[
///                     const Text('Modal BottomSheet'),
625
///                     ElevatedButton(
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
///                       child: const Text('Close BottomSheet'),
///                       onPressed: () => Navigator.pop(context),
///                     )
///                   ],
///                 ),
///               ),
///             );
///           },
///         );
///       },
///     ),
///   );
/// }
/// ```
/// {@end-tool}
641 642
/// See also:
///
643 644
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    function passed as the `builder` argument to [showModalBottomSheet].
645 646
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
647 648
///  * [DraggableScrollableSheet], which allows you to create a bottom sheet
///    that grows and then becomes scrollable once it reaches its maximum size.
649
///  * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet>
650 651 652
Future<T> showModalBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
653
  Color backgroundColor,
654 655
  double elevation,
  ShapeBorder shape,
656
  Clip clipBehavior,
657
  Color barrierColor,
658
  bool isScrollControlled = false,
659
  bool useRootNavigator = false,
660
  bool isDismissible = true,
661
  bool enableDrag = true,
662
  RouteSettings routeSettings,
663
}) {
664 665
  assert(context != null);
  assert(builder != null);
666
  assert(isScrollControlled != null);
667
  assert(useRootNavigator != null);
668
  assert(isDismissible != null);
669
  assert(enableDrag != null);
670
  assert(debugCheckHasMediaQuery(context));
671
  assert(debugCheckHasMaterialLocalizations(context));
672

673
  return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute<T>(
674 675
    builder: builder,
    theme: Theme.of(context, shadowThemeOnly: true),
676
    isScrollControlled: isScrollControlled,
677
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
678 679
    backgroundColor: backgroundColor,
    elevation: elevation,
680
    shape: shape,
681
    clipBehavior: clipBehavior,
682
    isDismissible: isDismissible,
683 684
    modalBarrierColor: barrierColor,
    enableDrag: enableDrag,
685
    settings: routeSettings,
686 687
  ));
}
688

689 690
/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If
/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet].
691
///
692 693 694
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
695 696 697 698
/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
/// parameters can be passed in to customize the appearance and behavior of
/// persistent bottom sheets.
///
699 700 701 702 703
/// 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
704
/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
705 706 707
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
708
/// does not add a back button to the enclosing Scaffold's app bar, use the
709 710
/// [Scaffold.bottomSheet] constructor parameter.
///
711 712 713 714 715 716 717 718 719 720 721
/// 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:
///
722 723
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    `builder`.
724 725 726
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * [Scaffold.of], for information about how to obtain the [BuildContext].
727
///  * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
728 729 730
PersistentBottomSheetController<T> showBottomSheet<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
731
  Color backgroundColor,
732 733
  double elevation,
  ShapeBorder shape,
734
  Clip clipBehavior,
735 736 737
}) {
  assert(context != null);
  assert(builder != null);
738 739 740 741 742
  assert(debugCheckHasScaffold(context));

  return Scaffold.of(context).showBottomSheet<T>(
    builder,
    backgroundColor: backgroundColor,
743 744
    elevation: elevation,
    shape: shape,
745
    clipBehavior: clipBehavior,
746
  );
747
}