bottom_sheet.dart 25.6 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
import 'package:flutter/scheduler.dart';
9 10
import 'package:flutter/widgets.dart';

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

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

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

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

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

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

class _BottomSheetState extends State<BottomSheet> {

187
  final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
188 189

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

194
  bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse;
195

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

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

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

    if (widget.onDragEnd != null) {
231
      widget.onDragEnd!(
232 233 234 235 236 237 238 239
        details,
        isClosing: isClosing,
      );
    }

    if (isClosing) {
      widget.onClosing();
    }
240 241 242 243 244
  }

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

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

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

278
// PERSISTENT BOTTOM SHEETS
279

280
// See scaffold.dart
281 282


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

  final double progress;
288
  final bool isScrollControlled;
289

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

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

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

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

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

335
  @override
336
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
337 338
}

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

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

355 356 357 358 359
  void handleDragStart(DragStartDetails details) {
    // Allow the bottom sheet to track the user's finger accurately.
    animationCurve = Curves.linear;
  }

360
  void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
361 362
    // Allow the bottom sheet to animate smoothly from its current position.
    animationCurve = _BottomSheetSuspendedCurve(
363
      widget.route!.animation!.value,
364 365 366 367
      curve: _modalBottomSheetCurve,
    );
  }

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

376
    return AnimatedBuilder(
377
      animation: widget.route!.animation!,
378
      child: BottomSheet(
379
        animationController: widget.route!._animationController,
380
        onClosing: () {
381
          if (widget.route!.isCurrent) {
382 383 384
            Navigator.pop(context);
          }
        },
385
        builder: widget.route!.builder!,
386 387 388 389 390 391 392 393
        backgroundColor: widget.backgroundColor,
        elevation: widget.elevation,
        shape: widget.shape,
        clipBehavior: widget.clipBehavior,
        enableDrag: widget.enableDrag,
        onDragStart: handleDragStart,
        onDragEnd: handleDragEnd,
      ),
394
      builder: (BuildContext context, Widget? child) {
395 396
        // Disable the initial animation when accessible navigation is on so
        // that the semantics are added to the tree at the correct time.
397
        final double animationValue = animationCurve.transform(
398
            mediaQuery.accessibleNavigation ? 1.0 : widget.route!.animation!.value
399
        );
400 401 402 403 404 405 406 407
        return Semantics(
          scopesRoute: true,
          namesRoute: true,
          label: routeLabel,
          explicitChildNodes: true,
          child: ClipRect(
            child: CustomSingleChildLayout(
              delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
408
              child: child,
409
            ),
410 411 412
          ),
        );
      },
413 414 415 416
    );
  }
}

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

436
  final WidgetBuilder? builder;
437
  final CapturedThemes capturedThemes;
438
  final bool isScrollControlled;
439 440 441 442 443
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
  final Color? modalBarrierColor;
444
  final bool isDismissible;
445
  final bool enableDrag;
446

447
  @override
448 449 450 451
  Duration get transitionDuration => _bottomSheetEnterDuration;

  @override
  Duration get reverseTransitionDuration => _bottomSheetExitDuration;
452 453

  @override
454
  bool get barrierDismissible => isDismissible;
455

456
  @override
457
  final String? barrierLabel;
458

459
  @override
460
  Color get barrierColor => modalBarrierColor ?? Colors.black54;
461

462
  AnimationController? _animationController;
463

464
  @override
465
  AnimationController createAnimationController() {
466
    assert(_animationController == null);
467 468
    _animationController = BottomSheet.createAnimationController(navigator!.overlay!);
    return _animationController!;
469 470
  }

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

497 498
// 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
499 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
/// 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);
546
    return lerpDouble(startingPoint, 1, transformed)!;
547 548 549 550 551 552 553 554
  }

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

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

672
  final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
673
  return navigator.push(_ModalBottomSheetRoute<T>(
674
    builder: builder,
675
    capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
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
PersistentBottomSheetController<T> showBottomSheet<T>({
729 730 731 732 733 734
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
735 736 737
}) {
  assert(context != null);
  assert(builder != null);
738 739
  assert(debugCheckHasScaffold(context));

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