bottom_sheet.dart 27.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 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
    assert(widget.enableDrag);
219 220
    if (_dismissUnderway)
      return;
221
    widget.animationController!.value -= details.primaryDelta! / _childHeight;
222 223
  }

224
  void _handleDragEnd(DragEndDetails details) {
225
    assert(widget.enableDrag);
226 227
    if (_dismissUnderway)
      return;
228
    bool isClosing = false;
229
    if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
230
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
231 232
      if (widget.animationController!.value > 0.0) {
        widget.animationController!.fling(velocity: flingVelocity);
233 234
      }
      if (flingVelocity < 0.0) {
235
        isClosing = true;
236
      }
237 238 239
    } else if (widget.animationController!.value < _closeProgressThreshold) {
      if (widget.animationController!.value > 0.0)
        widget.animationController!.fling(velocity: -1.0);
240
      isClosing = true;
241
    } else {
242
      widget.animationController!.forward();
243 244
    }

245 246 247 248
    widget.onDragEnd?.call(
      details,
      isClosing: isClosing,
    );
249 250 251 252

    if (isClosing) {
      widget.onClosing();
    }
253 254 255 256 257
  }

  bool extentChanged(DraggableScrollableNotification notification) {
    if (notification.extent == notification.minExtent) {
      widget.onClosing();
258
    }
259
    return false;
260 261
  }

262
  @override
263
  Widget build(BuildContext context) {
264
    final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
265
    final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints;
266
    final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
267
    final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? 0;
268
    final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape;
269
    final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
270

271
    Widget bottomSheet = Material(
272
      key: _childKey,
273 274 275
      color: color,
      elevation: elevation,
      shape: shape,
276
      clipBehavior: clipBehavior,
277 278 279 280
      child: NotificationListener<DraggableScrollableNotification>(
        onNotification: extentChanged,
        child: widget.builder(context),
      ),
281
    );
282 283 284 285

    if (constraints != null) {
      bottomSheet = Align(
        alignment: Alignment.bottomCenter,
286
        heightFactor: 1.0,
287 288 289 290 291 292 293
        child: ConstrainedBox(
          constraints: constraints,
          child: bottomSheet,
        ),
      );
    }

294
    return !widget.enableDrag ? bottomSheet : GestureDetector(
295
      onVerticalDragStart: _handleDragStart,
296
      onVerticalDragUpdate: _handleDragUpdate,
297
      onVerticalDragEnd: _handleDragEnd,
298
      excludeFromSemantics: true,
299
      child: bottomSheet,
300 301 302 303
    );
  }
}

304
// PERSISTENT BOTTOM SHEETS
305

306
// See scaffold.dart
307 308


309
// MODAL BOTTOM SHEETS
310
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
311
  _ModalBottomSheetLayout(this.progress, this.isScrollControlled);
312 313

  final double progress;
314
  final bool isScrollControlled;
315

316
  @override
317
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
318
    return BoxConstraints(
319 320 321
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
      minHeight: 0.0,
322 323 324
      maxHeight: isScrollControlled
        ? constraints.maxHeight
        : constraints.maxHeight * 9.0 / 16.0,
325 326 327
    );
  }

328
  @override
329
  Offset getPositionForChild(Size size, Size childSize) {
330
    return Offset(0.0, size.height - childSize.height * progress);
331 332
  }

333
  @override
334 335
  bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
    return progress != oldDelegate.progress;
336 337 338
  }
}

339
class _ModalBottomSheet<T> extends StatefulWidget {
340
  const _ModalBottomSheet({
341
    Key? key,
342
    this.route,
343 344 345
    this.backgroundColor,
    this.elevation,
    this.shape,
346
    this.clipBehavior,
347
    this.constraints,
348
    this.isScrollControlled = false,
349
    this.enableDrag = true,
350
  }) : assert(isScrollControlled != null),
351
       assert(enableDrag != null),
352
       super(key: key);
353

354
  final _ModalBottomSheetRoute<T>? route;
355
  final bool isScrollControlled;
356 357 358 359
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
360
  final BoxConstraints? constraints;
361
  final bool enableDrag;
362

363
  @override
364
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
365 366
}

367
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
368 369
  ParametricCurve<double> animationCurve = _modalBottomSheetCurve;

370
  String _getRouteLabel(MaterialLocalizations localizations) {
371
    switch (Theme.of(context).platform) {
372
      case TargetPlatform.iOS:
373
      case TargetPlatform.macOS:
374
        return '';
375 376
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
377 378
      case TargetPlatform.linux:
      case TargetPlatform.windows:
379
        return localizations.dialogLabel;
380
    }
381 382
  }

383 384 385 386 387
  void handleDragStart(DragStartDetails details) {
    // Allow the bottom sheet to track the user's finger accurately.
    animationCurve = Curves.linear;
  }

388
  void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
389 390
    // Allow the bottom sheet to animate smoothly from its current position.
    animationCurve = _BottomSheetSuspendedCurve(
391
      widget.route!.animation!.value,
392 393 394 395
      curve: _modalBottomSheetCurve,
    );
  }

396 397 398 399
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasMaterialLocalizations(context));
400
    final MediaQueryData mediaQuery = MediaQuery.of(context);
401
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
402
    final String routeLabel = _getRouteLabel(localizations);
403

404
    return AnimatedBuilder(
405
      animation: widget.route!.animation!,
406 407 408 409 410 411 412 413 414 415 416 417
      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,
418
        constraints: widget.constraints,
419 420 421
        enableDrag: widget.enableDrag,
        onDragStart: handleDragStart,
        onDragEnd: handleDragEnd,
422
      ),
423
      builder: (BuildContext context, Widget? child) {
424 425
        // Disable the initial animation when accessible navigation is on so
        // that the semantics are added to the tree at the correct time.
426
        final double animationValue = animationCurve.transform(
427
            mediaQuery.accessibleNavigation ? 1.0 : widget.route!.animation!.value,
428
        );
429 430 431 432 433 434 435 436
        return Semantics(
          scopesRoute: true,
          namesRoute: true,
          label: routeLabel,
          explicitChildNodes: true,
          child: ClipRect(
            child: CustomSingleChildLayout(
              delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
437
              child: child,
438
            ),
439 440 441
          ),
        );
      },
442 443 444 445
    );
  }
}

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

467
  final WidgetBuilder? builder;
468
  final CapturedThemes capturedThemes;
469
  final bool isScrollControlled;
470 471 472 473
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
474
  final BoxConstraints? constraints;
475
  final Color? modalBarrierColor;
476
  final bool isDismissible;
477
  final bool enableDrag;
478
  final AnimationController? transitionAnimationController;
479

480
  @override
481 482 483 484
  Duration get transitionDuration => _bottomSheetEnterDuration;

  @override
  Duration get reverseTransitionDuration => _bottomSheetExitDuration;
485 486

  @override
487
  bool get barrierDismissible => isDismissible;
488

489
  @override
490
  final String? barrierLabel;
491

492
  @override
493
  Color get barrierColor => modalBarrierColor ?? Colors.black54;
494

495
  AnimationController? _animationController;
496

497
  @override
498
  AnimationController createAnimationController() {
499
    assert(_animationController == null);
500
    _animationController = transitionAnimationController ?? BottomSheet.createAnimationController(navigator!.overlay!);
501
    return _animationController!;
502 503
  }

504
  @override
505
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
506 507
    // By definition, the bottom sheet is aligned to the bottom of the page
    // and isn't exposed to the top padding of the MediaQuery.
508
    final Widget bottomSheet = MediaQuery.removePadding(
509 510
      context: context,
      removeTop: true,
511 512
      child: Builder(
        builder: (BuildContext context) {
513
          final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
514 515 516 517 518 519
          return _ModalBottomSheet<T>(
            route: this,
            backgroundColor: backgroundColor ?? sheetTheme.modalBackgroundColor ?? sheetTheme.backgroundColor,
            elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
            shape: shape,
            clipBehavior: clipBehavior,
520
            constraints: constraints,
521 522 523 524
            isScrollControlled: isScrollControlled,
            enableDrag: enableDrag,
          );
        },
525
      ),
526
    );
527
    return capturedThemes.wrap(bottomSheet);
528 529 530
  }
}

531 532
// 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
533 534 535 536 537 538 539 540 541 542 543 544 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
/// 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);
580
    return lerpDouble(startingPoint, 1, transformed)!;
581 582 583 584 585 586 587 588
  }

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

589 590 591 592 593 594 595 596
/// 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
597 598
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
599
///
Ian Hickson's avatar
Ian Hickson committed
600 601 602 603 604
/// 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.
///
605 606 607 608
/// 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
609
/// parameter to true.
610
///
611 612 613 614 615
/// 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].
///
616 617 618
/// The [isDismissible] parameter specifies whether the bottom sheet will be
/// dismissed when user taps on the scrim.
///
619
/// The [enableDrag] parameter specifies whether the bottom sheet can be
620
/// dragged up and down and dismissed by swiping downwards.
621
///
622 623
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
624
/// parameters can be passed in to customize the appearance and behavior of
625 626
/// modal bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
627
///
628 629 630
/// The [transitionAnimationController] controls the bottom sheet's entrance and
/// exit animations if provided.
///
631 632 633 634
/// 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].
///
635
/// Returns a `Future` that resolves to the value (if any) that was passed to
636
/// [Navigator.pop] when the modal bottom sheet was closed.
637
///
638
/// {@tool dartpad --template=stateless_widget_scaffold}
639 640 641 642 643 644 645 646 647
///
/// 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(
648
///     child: ElevatedButton(
649 650 651 652 653 654 655 656 657 658 659 660 661 662
///       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'),
663
///                     ElevatedButton(
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
///                       child: const Text('Close BottomSheet'),
///                       onPressed: () => Navigator.pop(context),
///                     )
///                   ],
///                 ),
///               ),
///             );
///           },
///         );
///       },
///     ),
///   );
/// }
/// ```
/// {@end-tool}
679 680
/// See also:
///
681 682
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    function passed as the `builder` argument to [showModalBottomSheet].
683 684
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
685 686
///  * [DraggableScrollableSheet], which allows you to create a bottom sheet
///    that grows and then becomes scrollable once it reaches its maximum size.
687
///  * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet>
688
Future<T?> showModalBottomSheet<T>({
689 690 691 692 693 694
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
695
  BoxConstraints? constraints,
696
  Color? barrierColor,
697
  bool isScrollControlled = false,
698
  bool useRootNavigator = false,
699
  bool isDismissible = true,
700
  bool enableDrag = true,
701
  RouteSettings? routeSettings,
702
  AnimationController? transitionAnimationController,
703
}) {
704 705
  assert(context != null);
  assert(builder != null);
706
  assert(isScrollControlled != null);
707
  assert(useRootNavigator != null);
708
  assert(isDismissible != null);
709
  assert(enableDrag != null);
710
  assert(debugCheckHasMediaQuery(context));
711
  assert(debugCheckHasMaterialLocalizations(context));
712

713
  final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
714
  return navigator.push(_ModalBottomSheetRoute<T>(
715
    builder: builder,
716
    capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
717
    isScrollControlled: isScrollControlled,
718
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
719 720
    backgroundColor: backgroundColor,
    elevation: elevation,
721
    shape: shape,
722
    clipBehavior: clipBehavior,
723
    constraints: constraints,
724
    isDismissible: isDismissible,
725 726
    modalBarrierColor: barrierColor,
    enableDrag: enableDrag,
727
    settings: routeSettings,
728
    transitionAnimationController: transitionAnimationController,
729 730
  ));
}
731

732 733
/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If
/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet].
734
///
735 736 737
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
738 739
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
740
/// parameters can be passed in to customize the appearance and behavior of
741 742
/// persistent bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
743
///
744 745 746 747 748
/// 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
749
/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
750 751 752
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
753
/// does not add a back button to the enclosing Scaffold's app bar, use the
754 755
/// [Scaffold.bottomSheet] constructor parameter.
///
756 757 758 759 760 761 762 763 764 765 766
/// 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:
///
767 768
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    `builder`.
769 770 771
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * [Scaffold.of], for information about how to obtain the [BuildContext].
772
///  * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
773
PersistentBottomSheetController<T> showBottomSheet<T>({
774 775 776 777 778 779
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
780
  BoxConstraints? constraints,
781
  AnimationController? transitionAnimationController,
782 783 784
}) {
  assert(context != null);
  assert(builder != null);
785 786
  assert(debugCheckHasScaffold(context));

787
  return Scaffold.of(context).showBottomSheet<T>(
788 789
    builder,
    backgroundColor: backgroundColor,
790 791
    elevation: elevation,
    shape: shape,
792
    clipBehavior: clipBehavior,
793
    constraints: constraints,
794
    transitionAnimationController: transitionAnimationController,
795
  );
796
}