bottom_sheet.dart 51.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/gestures.dart';
9
import 'package:flutter/rendering.dart';
10 11
import 'package:flutter/widgets.dart';

12
import 'bottom_sheet_theme.dart';
13
import 'color_scheme.dart';
14
import 'colors.dart';
15
import 'constants.dart';
16
import 'curves.dart';
17
import 'debug.dart';
18
import 'material.dart';
19
import 'material_localizations.dart';
20
import 'material_state.dart';
21
import 'scaffold.dart';
22
import 'theme.dart';
23

24 25 26
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
const Curve _modalBottomSheetCurve = decelerateEasing;
27 28
const double _minFlingVelocity = 700.0;
const double _closeProgressThreshold = 0.5;
29
const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0;
30

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

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

44
/// A Material Design bottom sheet.
45
///
46
/// There are two kinds of bottom sheets in Material Design:
47 48 49 50 51
///
///  * _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
52 53
///    [ScaffoldState.showBottomSheet] function or by specifying the
///    [Scaffold.bottomSheet] constructor parameter.
54 55 56 57 58 59 60
///
///  * _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
61 62
/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
63 64 65
///
/// See also:
///
66 67 68 69
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal "persistent" bottom sheets.
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
70 71
///  * [BottomSheetThemeData], which can be used to customize the default
///    bottom sheet property values.
72 73
///  * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
///  * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
74
class BottomSheet extends StatefulWidget {
75 76 77
  /// Creates a bottom sheet.
  ///
  /// Typically, bottom sheets are created implicitly by
78
  /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
79
  /// [showModalBottomSheet], for modal bottom sheets.
80
  const BottomSheet({
81
    super.key,
82
    this.animationController,
83
    this.enableDrag = true,
84 85 86
    this.showDragHandle,
    this.dragHandleColor,
    this.dragHandleSize,
87 88
    this.onDragStart,
    this.onDragEnd,
89
    this.backgroundColor,
90
    this.shadowColor,
91 92
    this.elevation,
    this.shape,
93
    this.clipBehavior,
94
    this.constraints,
95 96
    required this.onClosing,
    required this.builder,
97
  }) : assert(elevation == null || elevation >= 0.0);
98

99 100
  /// The animation controller that controls the bottom sheet's entrance and
  /// exit animations.
101 102 103
  ///
  /// The BottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
104
  final AnimationController? animationController;
105 106 107

  /// Called when the bottom sheet begins to close.
  ///
108
  /// A bottom sheet might be prevented from closing (e.g., by user
109 110
  /// interaction) even after this callback is called. For this reason, this
  /// callback might be call multiple times for a given bottom sheet.
111
  final VoidCallback onClosing;
112 113 114 115 116

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

119
  /// If true, the bottom sheet can be dragged up and down and dismissed by
120
  /// swiping downwards.
121
  ///
122 123 124
  /// If [showDragHandle] is true, this only applies to the content below the drag handle,
  /// because the drag handle is always draggable.
  ///
125
  /// Default is true.
126 127 128 129
  ///
  /// If this is true, the [animationController] must not be null.
  /// Use [BottomSheet.createAnimationController] to create one, or provide
  /// another AnimationController.
130 131
  final bool enableDrag;

132 133 134 135 136 137 138 139 140
  /// Specifies whether a drag handle is shown.
  ///
  /// The drag handle appears at the top of the bottom sheet. The default color is
  /// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
  /// using [dragHandleColor]. The default size is `Size(32,4)` and can be customized
  /// with [dragHandleSize].
  ///
  /// If null, then the value of  [BottomSheetThemeData.showDragHandle] is used. If
  /// that is also null, defaults to false.
141 142 143 144
  ///
  /// If this is true, the [animationController] must not be null.
  /// Use [BottomSheet.createAnimationController] to create one, or provide
  /// another AnimationController.
145 146 147 148 149 150 151 152 153 154 155 156 157
  final bool? showDragHandle;

  /// The bottom sheet drag handle's color.
  ///
  /// Defaults to [BottomSheetThemeData.dragHandleColor].
  /// If that is also null, defaults to [ColorScheme.onSurfaceVariant]
  /// with an opacity of 0.4.
  final Color? dragHandleColor;

  /// Defaults to [BottomSheetThemeData.dragHandleSize].
  /// If that is also null, defaults to Size(32, 4).
  final Size? dragHandleSize;

158 159 160 161 162
  /// 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.
163
  final BottomSheetDragStartHandler? onDragStart;
164 165 166 167 168 169 170

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

173 174 175 176 177
  /// The bottom sheet's background color.
  ///
  /// Defines the bottom sheet's [Material.color].
  ///
  /// Defaults to null and falls back to [Material]'s default.
178
  final Color? backgroundColor;
179

180 181 182 183 184 185 186 187 188 189 190 191
  /// The color of the shadow below the sheet.
  ///
  /// If this property is null, then [BottomSheetThemeData.shadowColor] of
  /// [ThemeData.bottomSheetTheme] is used. If that is also null, the default value
  /// is transparent.
  ///
  /// See also:
  ///
  ///  * [elevation], which defines the size of the shadow below the sheet.
  ///  * [shape], which defines the shape of the sheet and its shadow.
  final Color? shadowColor;

192
  /// The z-coordinate at which to place this material relative to its parent.
193
  ///
194 195 196
  /// This controls the size of the shadow below the material.
  ///
  /// Defaults to 0. The value is non-negative.
197
  final double? elevation;
198

199
  /// The shape of the bottom sheet.
200
  ///
201 202 203
  /// Defines the bottom sheet's [Material.shape].
  ///
  /// Defaults to null and falls back to [Material]'s default.
204
  final ShapeBorder? shape;
205

206
  /// {@macro flutter.material.Material.clipBehavior}
207 208 209 210 211 212 213 214
  ///
  /// 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.
  ///
215
  /// If this property is null then [BottomSheetThemeData.clipBehavior] of
216 217
  /// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior
  /// will be [Clip.none].
218
  final Clip? clipBehavior;
219

220 221 222 223
  /// Defines minimum and maximum sizes for a [BottomSheet].
  ///
  /// If null, then the ambient [ThemeData.bottomSheetTheme]'s
  /// [BottomSheetThemeData.constraints] will be used. If that
224 225 226 227 228
  /// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
  /// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
  /// the bottom sheet's size will be constrained by its parent
  /// (usually a [Scaffold]). In this case, consider limiting the width by
  /// setting smaller constraints for large screens.
229 230 231 232 233 234
  ///
  /// 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;

235
  @override
236
  State<BottomSheet> createState() => _BottomSheetState();
237

238 239 240 241 242 243
  /// 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.
244
  static AnimationController createAnimationController(TickerProvider vsync) {
245
    return AnimationController(
246 247
      duration: _bottomSheetEnterDuration,
      reverseDuration: _bottomSheetExitDuration,
248 249
      debugLabel: 'BottomSheet',
      vsync: vsync,
250 251
    );
  }
252 253 254 255
}

class _BottomSheetState extends State<BottomSheet> {

256
  final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
257 258

  double get _childHeight {
259
    final RenderBox renderBox = _childKey.currentContext!.findRenderObject()! as RenderBox;
260 261
    return renderBox.size.height;
  }
262

263
  bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse;
264

265 266
  Set<MaterialState> dragHandleMaterialState = <MaterialState>{};

267
  void _handleDragStart(DragStartDetails details) {
268 269 270
    setState(() {
      dragHandleMaterialState.add(MaterialState.dragged);
    });
271
    widget.onDragStart?.call(details);
272 273
  }

274
  void _handleDragUpdate(DragUpdateDetails details) {
275
    assert(
276 277
      (widget.enableDrag || (widget.showDragHandle?? false)) && widget.animationController != null,
      "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
278 279
      "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
    );
280
    if (_dismissUnderway) {
281
      return;
282
    }
283
    widget.animationController!.value -= details.primaryDelta! / _childHeight;
284 285
  }

286
  void _handleDragEnd(DragEndDetails details) {
287
    assert(
288 289
      (widget.enableDrag || (widget.showDragHandle?? false)) && widget.animationController != null,
      "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
290 291
      "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
    );
292
    if (_dismissUnderway) {
293
      return;
294
    }
295 296 297
    setState(() {
      dragHandleMaterialState.remove(MaterialState.dragged);
    });
298
    bool isClosing = false;
299
    if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
300
      final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
301 302
      if (widget.animationController!.value > 0.0) {
        widget.animationController!.fling(velocity: flingVelocity);
303 304
      }
      if (flingVelocity < 0.0) {
305
        isClosing = true;
306
      }
307
    } else if (widget.animationController!.value < _closeProgressThreshold) {
308
      if (widget.animationController!.value > 0.0) {
309
        widget.animationController!.fling(velocity: -1.0);
310
      }
311
      isClosing = true;
312
    } else {
313
      widget.animationController!.forward();
314 315
    }

316 317 318 319
    widget.onDragEnd?.call(
      details,
      isClosing: isClosing,
    );
320 321 322 323

    if (isClosing) {
      widget.onClosing();
    }
324 325 326
  }

  bool extentChanged(DraggableScrollableNotification notification) {
327
    if (notification.extent == notification.minExtent && notification.shouldCloseOnMinExtent) {
328
      widget.onClosing();
329
    }
330
    return false;
331 332
  }

333 334 335
  void _handleDragHandleHover(bool hovering) {
    if (hovering != dragHandleMaterialState.contains(MaterialState.hovered)) {
      setState(() {
336
        if (hovering){
337 338 339 340 341 342 343 344 345
          dragHandleMaterialState.add(MaterialState.hovered);
        }
        else{
          dragHandleMaterialState.remove(MaterialState.hovered);
        }
      });
    }
  }

346
  @override
347
  Widget build(BuildContext context) {
348
    final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
349 350 351
    final bool useMaterial3 = Theme.of(context).useMaterial3;
    final BottomSheetThemeData defaults = useMaterial3 ? _BottomSheetDefaultsM3(context) : const BottomSheetThemeData();
    final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints ?? defaults.constraints;
352 353
    final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor;
    final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
354
    final Color? shadowColor = widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor;
355 356
    final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0;
    final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
357
    final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
358 359 360 361 362 363 364 365 366 367 368
    final bool showDragHandle = widget.showDragHandle ?? (widget.enableDrag && (bottomSheetTheme.showDragHandle ?? false));

    Widget? dragHandle;
    if (showDragHandle){
      dragHandle = _DragHandle(
        onSemanticsTap: widget.onClosing,
        handleHover: _handleDragHandleHover,
        materialState: dragHandleMaterialState,
        dragHandleColor: widget.dragHandleColor,
        dragHandleSize: widget.dragHandleSize,
      );
369
      // Only add [_BottomSheetGestureDetector] to the drag handle when the rest of the
370 371
      // bottom sheet is not draggable. If the whole bottom sheet is draggable,
      // no need to add it.
372
      if (!widget.enableDrag) {
373
        dragHandle = _BottomSheetGestureDetector(
374 375 376 377 378 379 380
          onVerticalDragStart: _handleDragStart,
          onVerticalDragUpdate: _handleDragUpdate,
          onVerticalDragEnd: _handleDragEnd,
          child: dragHandle,
        );
      }
    }
381

382
    Widget bottomSheet = Material(
383
      key: _childKey,
384 385
      color: color,
      elevation: elevation,
386
      surfaceTintColor: surfaceTintColor,
387
      shadowColor: shadowColor,
388
      shape: shape,
389
      clipBehavior: clipBehavior,
390 391
      child: NotificationListener<DraggableScrollableNotification>(
        onNotification: extentChanged,
392 393 394 395 396 397 398 399 400 401 402 403
        child: !showDragHandle
          ? widget.builder(context)
          : Stack(
              alignment: Alignment.topCenter,
              children: <Widget>[
                dragHandle!,
                Padding(
                  padding: const EdgeInsets.only(top: kMinInteractiveDimension),
                  child: widget.builder(context),
                ),
              ],
            ),
404
      ),
405
    );
406 407 408 409

    if (constraints != null) {
      bottomSheet = Align(
        alignment: Alignment.bottomCenter,
410
        heightFactor: 1.0,
411 412 413 414 415 416 417
        child: ConstrainedBox(
          constraints: constraints,
          child: bottomSheet,
        ),
      );
    }

418
    return !widget.enableDrag ? bottomSheet : _BottomSheetGestureDetector(
419
      onVerticalDragStart: _handleDragStart,
420
      onVerticalDragUpdate: _handleDragUpdate,
421
      onVerticalDragEnd: _handleDragEnd,
422
      child: bottomSheet,
423 424 425 426
    );
  }
}

427
// PERSISTENT BOTTOM SHEETS
428

429
// See scaffold.dart
430

431
typedef _SizeChangeCallback<Size> = void Function(Size);
432

433 434 435 436 437 438 439 440 441 442
class _DragHandle extends StatelessWidget {
  const _DragHandle({
    required this.onSemanticsTap,
    required this.handleHover,
    required this.materialState,
    this.dragHandleColor,
    this.dragHandleSize,
  });

  final VoidCallback? onSemanticsTap;
443
  final ValueChanged<bool> handleHover;
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
  final Set<MaterialState> materialState;
  final Color? dragHandleColor;
  final Size? dragHandleSize;

  @override
  Widget build(BuildContext context) {
    final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
    final BottomSheetThemeData m3Defaults = _BottomSheetDefaultsM3(context);
    final Size handleSize = dragHandleSize ?? bottomSheetTheme.dragHandleSize ?? m3Defaults.dragHandleSize!;

    return MouseRegion(
      onEnter: (PointerEnterEvent event) => handleHover(true),
      onExit: (PointerExitEvent event) => handleHover(false),
      child: Semantics(
        label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
        container: true,
        onTap: onSemanticsTap,
        child: SizedBox(
          height: kMinInteractiveDimension,
          width: kMinInteractiveDimension,
          child: Center(
            child: Container(
              height: handleSize.height,
              width: handleSize.width,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(handleSize.height/2),
                color: MaterialStateProperty.resolveAs<Color?>(dragHandleColor, materialState)
                  ?? MaterialStateProperty.resolveAs<Color?>(bottomSheetTheme.dragHandleColor, materialState)
                  ?? m3Defaults.dragHandleColor,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

482 483
class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
  const _BottomSheetLayoutWithSizeListener({
484
    required this.onChildSizeChanged,
485 486
    required this.animationValue,
    required this.isScrollControlled,
487
    required this.scrollControlDisabledMaxHeightRatio,
488
    super.child,
489
  });
490

491
  final _SizeChangeCallback<Size> onChildSizeChanged;
492
  final double animationValue;
493
  final bool isScrollControlled;
494
  final double scrollControlDisabledMaxHeightRatio;
495 496 497 498

  @override
  _RenderBottomSheetLayoutWithSizeListener createRenderObject(BuildContext context) {
    return _RenderBottomSheetLayoutWithSizeListener(
499
      onChildSizeChanged: onChildSizeChanged,
500 501
      animationValue: animationValue,
      isScrollControlled: isScrollControlled,
502
      scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
503 504 505 506 507 508 509 510
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderBottomSheetLayoutWithSizeListener renderObject) {
    renderObject.onChildSizeChanged = onChildSizeChanged;
    renderObject.animationValue = animationValue;
    renderObject.isScrollControlled = isScrollControlled;
511
    renderObject.scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio;
512 513 514 515 516 517 518 519 520
  }
}

class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
  _RenderBottomSheetLayoutWithSizeListener({
    RenderBox? child,
    required _SizeChangeCallback<Size> onChildSizeChanged,
    required double animationValue,
    required bool isScrollControlled,
521 522 523
    required double scrollControlDisabledMaxHeightRatio,
  }) : _onChildSizeChanged = onChildSizeChanged,
       _animationValue = animationValue,
524
       _isScrollControlled = isScrollControlled,
525
       _scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio,
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 556 557 558 559 560 561 562
       super(child);

  Size _lastSize = Size.zero;

  _SizeChangeCallback<Size> get onChildSizeChanged => _onChildSizeChanged;
  _SizeChangeCallback<Size> _onChildSizeChanged;
    set onChildSizeChanged(_SizeChangeCallback<Size> newCallback) {
    if (_onChildSizeChanged == newCallback) {
      return;
    }

    _onChildSizeChanged = newCallback;
    markNeedsLayout();
  }

  double get animationValue => _animationValue;
  double _animationValue;
  set animationValue(double newValue) {
    if (_animationValue == newValue) {
      return;
    }

    _animationValue = newValue;
    markNeedsLayout();
  }

  bool get isScrollControlled => _isScrollControlled;
  bool _isScrollControlled;
  set isScrollControlled(bool newValue) {
    if (_isScrollControlled == newValue) {
      return;
    }

    _isScrollControlled = newValue;
    markNeedsLayout();
  }

563 564 565 566 567 568 569 570 571 572 573
  double get scrollControlDisabledMaxHeightRatio => _scrollControlDisabledMaxHeightRatio;
  double _scrollControlDisabledMaxHeightRatio;
  set scrollControlDisabledMaxHeightRatio(double newValue) {
    if (_scrollControlDisabledMaxHeightRatio == newValue) {
      return;
    }

    _scrollControlDisabledMaxHeightRatio = newValue;
    markNeedsLayout();
  }

574 575 576 577 578 579 580 581 582 583 584 585
  Size _getSize(BoxConstraints constraints) {
    return constraints.constrain(constraints.biggest);
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite) {
      return width;
    }
    return 0.0;
  }
586

587
  @override
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
  double computeMaxIntrinsicWidth(double height) {
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite) {
      return width;
    }
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite) {
      return height;
    }
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite) {
      return height;
    }
    return 0.0;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _getSize(constraints);
  }

619
  BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
620
    return BoxConstraints(
621 622
      minWidth: constraints.maxWidth,
      maxWidth: constraints.maxWidth,
623
      maxHeight: isScrollControlled
624 625
          ? constraints.maxHeight
          : constraints.maxHeight * scrollControlDisabledMaxHeightRatio,
626 627 628
    );
  }

629 630
  Offset _getPositionForChild(Size size, Size childSize) {
    return Offset(0.0, size.height - childSize.height * animationValue);
631 632
  }

633
  @override
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648
  void performLayout() {
    size = _getSize(constraints);
    if (child != null) {
      final BoxConstraints childConstraints = _getConstraintsForChild(constraints);
      assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
      child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
      final BoxParentData childParentData = child!.parentData! as BoxParentData;
      childParentData.offset = _getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size);
      final Size childSize = childConstraints.isTight ? childConstraints.smallest : child!.size;

      if (_lastSize != childSize) {
        _lastSize = childSize;
        _onChildSizeChanged.call(_lastSize);
      }
    }
649 650 651
  }
}

652
class _ModalBottomSheet<T> extends StatefulWidget {
653
  const _ModalBottomSheet({
654
    super.key,
655
    required this.route,
656 657 658
    this.backgroundColor,
    this.elevation,
    this.shape,
659
    this.clipBehavior,
660
    this.constraints,
661
    this.isScrollControlled = false,
662
    this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
663
    this.enableDrag = true,
664
    this.showDragHandle = false,
665
  });
666

667
  final ModalBottomSheetRoute<T> route;
668
  final bool isScrollControlled;
669
  final double scrollControlDisabledMaxHeightRatio;
670 671 672 673
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
674
  final BoxConstraints? constraints;
675
  final bool enableDrag;
676
  final bool showDragHandle;
677

678
  @override
679
  _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
680 681
}

682
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
683 684
  ParametricCurve<double> animationCurve = _modalBottomSheetCurve;

685
  String _getRouteLabel(MaterialLocalizations localizations) {
686
    switch (Theme.of(context).platform) {
687
      case TargetPlatform.iOS:
688
      case TargetPlatform.macOS:
689
        return '';
690 691
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
692 693
      case TargetPlatform.linux:
      case TargetPlatform.windows:
694
        return localizations.dialogLabel;
695
    }
696 697
  }

698 699 700 701
  EdgeInsets _getNewClipDetails(Size topLayerSize) {
    return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
  }

702 703 704 705 706
  void handleDragStart(DragStartDetails details) {
    // Allow the bottom sheet to track the user's finger accurately.
    animationCurve = Curves.linear;
  }

707
  void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
708 709
    // Allow the bottom sheet to animate smoothly from its current position.
    animationCurve = _BottomSheetSuspendedCurve(
710
      widget.route.animation!.value,
711 712 713 714
      curve: _modalBottomSheetCurve,
    );
  }

715 716 717 718
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasMaterialLocalizations(context));
719
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
720
    final String routeLabel = _getRouteLabel(localizations);
721

722
    return AnimatedBuilder(
723
      animation: widget.route.animation!,
724
      child: BottomSheet(
725
        animationController: widget.route._animationController,
726
        onClosing: () {
727
          if (widget.route.isCurrent) {
728 729 730
            Navigator.pop(context);
          }
        },
731
        builder: widget.route.builder,
732 733 734 735
        backgroundColor: widget.backgroundColor,
        elevation: widget.elevation,
        shape: widget.shape,
        clipBehavior: widget.clipBehavior,
736
        constraints: widget.constraints,
737
        enableDrag: widget.enableDrag,
738
        showDragHandle: widget.showDragHandle,
739 740
        onDragStart: handleDragStart,
        onDragEnd: handleDragEnd,
741
      ),
742
      builder: (BuildContext context, Widget? child) {
743
        final double animationValue = animationCurve.transform(
744
            widget.route.animation!.value,
745
        );
746 747 748 749 750 751
        return Semantics(
          scopesRoute: true,
          namesRoute: true,
          label: routeLabel,
          explicitChildNodes: true,
          child: ClipRect(
752 753 754 755 756 757 758 759
            child: _BottomSheetLayoutWithSizeListener(
              onChildSizeChanged: (Size size) {
                widget.route._didChangeBarrierSemanticsClip(
                  _getNewClipDetails(size),
                );
              },
              animationValue: animationValue,
              isScrollControlled: widget.isScrollControlled,
760
              scrollControlDisabledMaxHeightRatio: widget.scrollControlDisabledMaxHeightRatio,
761
              child: child,
762
            ),
763 764 765
          ),
        );
      },
766 767 768 769
    );
  }
}

770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
/// A route that represents a Material Design modal bottom sheet.
///
/// {@template flutter.material.ModalBottomSheetRoute}
/// 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 user from interacting with the app. Persistent bottom sheets
/// can be created and displayed with the [showBottomSheet] function or the
/// [ScaffoldState.showBottomSheet] method.
///
/// The [isScrollControlled] parameter specifies whether this is a route for
/// a bottom sheet that will utilize [DraggableScrollableSheet]. Consider
/// setting this parameter to true if this bottom sheet has
/// a scrollable child, such as a [ListView] or a [GridView],
/// to have the bottom sheet be draggable.
///
/// The [isDismissible] parameter specifies whether the bottom sheet will be
/// dismissed when user taps on the scrim.
///
/// The [enableDrag] parameter specifies whether the bottom sheet can be
/// dragged up and down and dismissed by swiping downwards.
///
794
/// The [useSafeArea] parameter specifies whether the sheet will avoid system
795 796 797 798
/// intrusions on the top, left, and right. If false, no [SafeArea] is added;
/// and [MediaQuery.removePadding] is applied to the top,
/// so that system intrusions at the top will not be avoided by a [SafeArea]
/// inside the bottom sheet either.
799
/// Defaults to false.
800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828
///
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
/// parameters can be passed in to customize the appearance and behavior of
/// modal bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
///
/// The [transitionAnimationController] controls the bottom sheet's entrance and
/// exit animations. It's up to the owner of the controller to call
/// [AnimationController.dispose] when the controller is no longer needed.
///
/// The optional `settings` 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].
/// {@endtemplate}
///
/// {@macro flutter.widgets.RawDialogRoute}
///
/// See also:
///
///  * [showModalBottomSheet], which is a way to display a ModalBottomSheetRoute.
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    function passed as the `builder` argument to [showModalBottomSheet].
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
///  * [DraggableScrollableSheet], creates a bottom sheet that grows
///    and then becomes scrollable once it reaches its maximum size.
///  * [DisplayFeatureSubScreen], which documents the specifics of how
///    [DisplayFeature]s can split the screen into sub-screens.
829 830
///  * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
///  * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
831 832 833 834 835
class ModalBottomSheetRoute<T> extends PopupRoute<T> {
  /// A modal bottom sheet route.
  ModalBottomSheetRoute({
    required this.builder,
    this.capturedThemes,
836
    this.barrierLabel,
837
    this.barrierOnTapHint,
838
    this.backgroundColor,
839 840
    this.elevation,
    this.shape,
841
    this.clipBehavior,
842
    this.constraints,
843
    this.modalBarrierColor,
844
    this.isDismissible = true,
845
    this.enableDrag = true,
846
    this.showDragHandle,
847
    required this.isScrollControlled,
848
    this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
849
    super.settings,
850
    this.transitionAnimationController,
851
    this.anchorPoint,
852
    this.useSafeArea = false,
853
  });
854

855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873
  /// A builder for the contents of the sheet.
  ///
  /// The bottom sheet will wrap the widget produced by this builder in a
  /// [Material] widget.
  final WidgetBuilder builder;

  /// Stores a list of captured [InheritedTheme]s that are wrapped around the
  /// bottom sheet.
  ///
  /// Consider setting this attribute when the [ModalBottomSheetRoute]
  /// is created through [Navigator.push] and its friends.
  final CapturedThemes? capturedThemes;

  /// Specifies whether this is a route for a bottom sheet that will utilize
  /// [DraggableScrollableSheet].
  ///
  /// Consider setting this parameter to true if this bottom sheet has
  /// a scrollable child, such as a [ListView] or a [GridView],
  /// to have the bottom sheet be draggable.
874
  final bool isScrollControlled;
875

876 877 878 879 880 881 882
  /// The max height constraint ratio for the bottom sheet
  /// when [isScrollControlled] set to false,
  /// no ratio will be applied when [isScrollControlled] set to true.
  ///
  /// Defaults to 9 / 16.
  final double scrollControlDisabledMaxHeightRatio;

883 884 885 886 887
  /// The bottom sheet's background color.
  ///
  /// Defines the bottom sheet's [Material.color].
  ///
  /// If this property is not provided, it falls back to [Material]'s default.
888
  final Color? backgroundColor;
889 890 891 892 893 894

  /// The z-coordinate at which to place this material relative to its parent.
  ///
  /// This controls the size of the shadow below the material.
  ///
  /// Defaults to 0, must not be negative.
895
  final double? elevation;
896 897 898 899 900 901

  /// The shape of the bottom sheet.
  ///
  /// Defines the bottom sheet's [Material.shape].
  ///
  /// If this property is not provided, it falls back to [Material]'s default.
902
  final ShapeBorder? shape;
903 904 905 906 907 908 909 910 911 912 913 914 915

  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// 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.
  ///
  /// If this property is null, the [BottomSheetThemeData.clipBehavior] of
  /// [ThemeData.bottomSheetTheme] is used. If that's null, the behavior defaults to [Clip.none]
  /// will be [Clip.none].
916
  final Clip? clipBehavior;
917 918 919 920 921

  /// Defines minimum and maximum sizes for a [BottomSheet].
  ///
  /// If null, the ambient [ThemeData.bottomSheetTheme]'s
  /// [BottomSheetThemeData.constraints] will be used. If that
922 923 924 925 926
  /// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
  /// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
  /// the bottom sheet's size will be constrained by its parent
  /// (usually a [Scaffold]). In this case, consider limiting the width by
  /// setting smaller constraints for large screens.
927 928 929 930
  ///
  /// 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.
931
  final BoxConstraints? constraints;
932 933 934 935 936

  /// Specifies the color of the modal barrier that darkens everything below the
  /// bottom sheet.
  ///
  /// Defaults to `Colors.black54` if not provided.
937
  final Color? modalBarrierColor;
938 939 940 941 942 943 944

  /// Specifies whether the bottom sheet will be dismissed
  /// when user taps on the scrim.
  ///
  /// If true, the bottom sheet will be dismissed when user taps on the scrim.
  ///
  /// Defaults to true.
945
  final bool isDismissible;
946 947 948 949 950 951 952

  /// Specifies whether the bottom sheet can be dragged up and down
  /// and dismissed by swiping downwards.
  ///
  /// If true, the bottom sheet can be dragged up and down and dismissed by
  /// swiping downwards.
  ///
953 954
  /// This applies to the content below the drag handle, if showDragHandle is true.
  ///
955
  /// Defaults is true.
956
  final bool enableDrag;
957

958 959 960 961 962 963 964 965 966 967 968
  /// Specifies whether a drag handle is shown.
  ///
  /// The drag handle appears at the top of the bottom sheet. The default color is
  /// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
  /// using dragHandleColor. The default size is `Size(32,4)` and can be customized
  /// with dragHandleSize.
  ///
  /// If null, then the value of  [BottomSheetThemeData.showDragHandle] is used. If
  /// that is also null, defaults to false.
  final bool? showDragHandle;

969 970 971 972 973
  /// The animation controller that controls the bottom sheet's entrance and
  /// exit animations.
  ///
  /// The BottomSheet widget will manipulate the position of this animation, it
  /// is not just a passive observer.
974
  final AnimationController? transitionAnimationController;
975 976

  /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
977
  final Offset? anchorPoint;
978

979
  /// Whether to avoid system intrusions on the top, left, and right.
980
  ///
981 982
  /// If true, a [SafeArea] is inserted to keep the bottom sheet away from
  /// system intrusions at the top, left, and right sides of the screen.
983
  ///
984 985 986 987 988 989 990 991
  /// If false, the bottom sheet will extend through any system intrusions
  /// at the top, left, and right.
  ///
  /// If false, then moreover [MediaQuery.removePadding] will be used
  /// to remove top padding, so that a [SafeArea] widget inside the bottom
  /// sheet will have no effect at the top edge. If this is undesired, consider
  /// setting [useSafeArea] to true. Alternatively, wrap the [SafeArea] in a
  /// [MediaQuery] that restates an ambient [MediaQueryData] from outside [builder].
992 993 994 995 996
  ///
  /// In either case, the bottom sheet extends all the way to the bottom of
  /// the screen, including any system intrusions.
  ///
  /// The default is false.
997
  final bool useSafeArea;
998

999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
  /// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
  /// The semantic hint text that informs users what will happen if they
  /// tap on the widget. Announced in the format of 'Double tap to ...'.
  ///
  /// If the field is null, the default hint will be used, which results in
  /// announcement of 'Double tap to activate'.
  /// {@endtemplate}
  ///
  /// See also:
  ///
  ///  * [barrierDismissible], which controls the behavior of the barrier when
  ///    tapped.
  ///  * [ModalBarrier], which uses this field as onTapHint when it has an onTap action.
  final String? barrierOnTapHint;

  final ValueNotifier<EdgeInsets> _clipDetailsNotifier = ValueNotifier<EdgeInsets>(EdgeInsets.zero);

1016 1017 1018 1019 1020 1021
  @override
  void dispose() {
    _clipDetailsNotifier.dispose();
    super.dispose();
  }

1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
  /// Updates the details regarding how the [SemanticsNode.rect] (focus) of
  /// the barrier for this [ModalBottomSheetRoute] should be clipped.
  ///
  /// returns true if the clipDetails did change and false otherwise.
  bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
    if (_clipDetailsNotifier.value == newClipDetails) {
      return false;
    }
    _clipDetailsNotifier.value = newClipDetails;
    return true;
  }

1034
  @override
1035 1036 1037 1038
  Duration get transitionDuration => _bottomSheetEnterDuration;

  @override
  Duration get reverseTransitionDuration => _bottomSheetExitDuration;
1039 1040

  @override
1041
  bool get barrierDismissible => isDismissible;
1042

1043
  @override
1044
  final String? barrierLabel;
1045

1046
  @override
1047
  Color get barrierColor => modalBarrierColor ?? Colors.black54;
1048

1049
  AnimationController? _animationController;
1050

1051
  @override
1052
  AnimationController createAnimationController() {
1053
    assert(_animationController == null);
1054 1055 1056 1057
    if (transitionAnimationController != null) {
      _animationController = transitionAnimationController;
      willDisposeAnimationController = false;
    } else {
1058
      _animationController = BottomSheet.createAnimationController(navigator!);
1059
    }
1060
    return _animationController!;
1061 1062
  }

1063
  @override
1064
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1065 1066 1067 1068 1069
    final Widget content = DisplayFeatureSubScreen(
      anchorPoint: anchorPoint,
      child: Builder(
        builder: (BuildContext context) {
          final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
1070
          final BottomSheetThemeData defaults = Theme.of(context).useMaterial3 ? _BottomSheetDefaultsM3(context) : const BottomSheetThemeData();
1071 1072
          return _ModalBottomSheet<T>(
            route: this,
1073
            backgroundColor: backgroundColor ?? sheetTheme.modalBackgroundColor ?? sheetTheme.backgroundColor ?? defaults.backgroundColor,
1074
            elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation ?? defaults.modalElevation,
1075 1076 1077 1078
            shape: shape,
            clipBehavior: clipBehavior,
            constraints: constraints,
            isScrollControlled: isScrollControlled,
1079
            scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
1080
            enableDrag: enableDrag,
1081
            showDragHandle: showDragHandle ?? (enableDrag && (sheetTheme.showDragHandle ?? false)),
1082 1083
          );
        },
1084
      ),
1085
    );
1086 1087

    final Widget bottomSheet = useSafeArea
1088
      ? SafeArea(bottom: false, child: content)
1089 1090 1091 1092 1093 1094
      : MediaQuery.removePadding(
          context: context,
          removeTop: true,
          child: content,
        );

1095
    return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
1096
  }
1097 1098 1099

  @override
  Widget buildModalBarrier() {
1100
    if (barrierColor.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
      assert(barrierColor != barrierColor.withOpacity(0.0));
      final Animation<Color?> color = animation!.drive(
        ColorTween(
          begin: barrierColor.withOpacity(0.0),
          end: barrierColor, // changedInternalState is called if barrierColor updates
        ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
      );
      return AnimatedModalBarrier(
        color: color,
        dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
        semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
        barrierSemanticsDismissible: semanticsDismissible,
        clipDetailsNotifier: _clipDetailsNotifier,
        semanticsOnTapHint: barrierOnTapHint,
      );
    } else {
      return ModalBarrier(
        dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
        semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
        barrierSemanticsDismissible: semanticsDismissible,
        clipDetailsNotifier: _clipDetailsNotifier,
        semanticsOnTapHint: barrierOnTapHint,
      );
    }
  }
1126 1127
}

1128 1129
// 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
1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
/// 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.
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
  /// Creates a suspended curve.
  const _BottomSheetSuspendedCurve(
    this.startingPoint, {
    this.curve = Curves.easeOutCubic,
1149
  });
1150 1151 1152 1153 1154

  /// The progress value at which [curve] should begin.
  final double startingPoint;

  /// The curve to use when [startingPoint] is reached.
1155 1156
  ///
  /// This defaults to [Curves.easeOutCubic].
1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173
  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);
1174
    return lerpDouble(startingPoint, 1, transformed)!;
1175 1176 1177 1178 1179 1180 1181 1182
  }

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

1183
/// Shows a modal Material Design bottom sheet.
1184
///
1185
/// {@macro flutter.material.ModalBottomSheetRoute}
1186
///
1187
/// {@macro flutter.widgets.RawDialogRoute}
1188
///
Ian Hickson's avatar
Ian Hickson committed
1189 1190 1191 1192 1193
/// 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.
///
1194 1195 1196 1197 1198
/// 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].
///
1199
/// Returns a `Future` that resolves to the value (if any) that was passed to
1200
/// [Navigator.pop] when the modal bottom sheet was closed.
1201
///
1202 1203 1204
/// The 'barrierLabel' parameter can be used to set a custom barrierlabel.
/// Will default to modalBarrierDismissLabel of context if not set.
///
1205
/// {@tool dartpad}
1206
/// This example demonstrates how to use [showModalBottomSheet] to display a
1207 1208 1209 1210
/// 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.
///
1211
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart **
1212
/// {@end-tool}
1213
///
1214 1215 1216 1217 1218 1219 1220
/// {@tool dartpad}
/// This sample shows the creation of [showModalBottomSheet], as described in:
/// https://m3.material.io/components/bottom-sheets/overview
///
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart **
/// {@end-tool}
///
1221 1222
/// See also:
///
1223 1224
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    function passed as the `builder` argument to [showModalBottomSheet].
1225 1226
///  * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
///    non-modal bottom sheets.
1227 1228
///  * [DraggableScrollableSheet], creates a bottom sheet that grows
///    and then becomes scrollable once it reaches its maximum size.
1229 1230
///  * [DisplayFeatureSubScreen], which documents the specifics of how
///    [DisplayFeature]s can split the screen into sub-screens.
1231 1232
///  * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
///  * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
1233
Future<T?> showModalBottomSheet<T>({
1234 1235 1236
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
1237
  String? barrierLabel,
1238 1239 1240
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
1241
  BoxConstraints? constraints,
1242
  Color? barrierColor,
1243
  bool isScrollControlled = false,
1244
  double scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
1245
  bool useRootNavigator = false,
1246
  bool isDismissible = true,
1247
  bool enableDrag = true,
1248
  bool? showDragHandle,
1249
  bool useSafeArea = false,
1250
  RouteSettings? routeSettings,
1251
  AnimationController? transitionAnimationController,
1252
  Offset? anchorPoint,
1253
}) {
1254
  assert(debugCheckHasMediaQuery(context));
1255
  assert(debugCheckHasMaterialLocalizations(context));
1256

1257
  final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
1258
  final MaterialLocalizations localizations = MaterialLocalizations.of(context);
1259
  return navigator.push(ModalBottomSheetRoute<T>(
1260
    builder: builder,
1261
    capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
1262
    isScrollControlled: isScrollControlled,
1263
    scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
1264
    barrierLabel: barrierLabel ?? localizations.scrimLabel,
1265
    barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel),
1266 1267
    backgroundColor: backgroundColor,
    elevation: elevation,
1268
    shape: shape,
1269
    clipBehavior: clipBehavior,
1270
    constraints: constraints,
1271
    isDismissible: isDismissible,
1272
    modalBarrierColor: barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
1273
    enableDrag: enableDrag,
1274
    showDragHandle: showDragHandle,
1275
    settings: routeSettings,
1276
    transitionAnimationController: transitionAnimationController,
1277
    anchorPoint: anchorPoint,
1278
    useSafeArea: useSafeArea,
1279 1280
  ));
}
1281

1282 1283
/// Shows a Material Design bottom sheet in the nearest [Scaffold] ancestor. To
/// show a persistent bottom sheet, use the [Scaffold.bottomSheet].
1284
///
1285 1286 1287
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
1288 1289
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
1290
/// parameters can be passed in to customize the appearance and behavior of
1291 1292
/// persistent bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
1293
///
1294 1295 1296
/// The [enableDrag] parameter specifies whether the bottom sheet can be
/// dragged up and down and dismissed by swiping downwards.
///
1297 1298 1299 1300 1301
/// 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
1302
/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
1303 1304 1305
/// that closes the bottom sheet.
///
/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
1306
/// does not add a back button to the enclosing Scaffold's app bar, use the
1307 1308
/// [Scaffold.bottomSheet] constructor parameter.
///
1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319
/// 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:
///
1320 1321
///  * [BottomSheet], which becomes the parent of the widget returned by the
///    `builder`.
1322 1323 1324
///  * [showModalBottomSheet], which can be used to display a modal bottom
///    sheet.
///  * [Scaffold.of], for information about how to obtain the [BuildContext].
1325 1326
///  * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
///  * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
1327
PersistentBottomSheetController showBottomSheet({
1328 1329 1330 1331 1332 1333
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
1334
  BoxConstraints? constraints,
1335
  bool? enableDrag,
1336
  AnimationController? transitionAnimationController,
1337
}) {
1338 1339
  assert(debugCheckHasScaffold(context));

1340
  return Scaffold.of(context).showBottomSheet(
1341 1342
    builder,
    backgroundColor: backgroundColor,
1343 1344
    elevation: elevation,
    shape: shape,
1345
    clipBehavior: clipBehavior,
1346
    constraints: constraints,
1347
    enableDrag: enableDrag,
1348
    transitionAnimationController: transitionAnimationController,
1349
  );
1350
}
1351

1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363
class _BottomSheetGestureDetector extends StatelessWidget {
  const _BottomSheetGestureDetector({
    required this.child,
    required this.onVerticalDragStart,
    required this.onVerticalDragUpdate,
    required this.onVerticalDragEnd,
  });

  final Widget child;
  final GestureDragStartCallback onVerticalDragStart;
  final GestureDragUpdateCallback onVerticalDragUpdate;
  final GestureDragEndCallback onVerticalDragEnd;
1364

1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      excludeFromSemantics: true,
      gestures: <Type, GestureRecognizerFactory<GestureRecognizer>>{
        VerticalDragGestureRecognizer : GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
          () => VerticalDragGestureRecognizer(debugOwner: this),
          (VerticalDragGestureRecognizer instance) {
            instance
              ..onStart = onVerticalDragStart
              ..onUpdate = onVerticalDragUpdate
              ..onEnd = onVerticalDragEnd
              ..onlyAcceptDragOnThreshold = true;
          },
        ),
      },
      child: child,
    );
  }
}
1385 1386 1387 1388 1389 1390 1391 1392 1393

// BEGIN GENERATED TOKEN PROPERTIES - BottomSheet

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
1394
  _BottomSheetDefaultsM3(this.context)
1395 1396 1397
    : super(
      elevation: 1.0,
      modalElevation: 1.0,
1398
      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))),
1399
      constraints: const BoxConstraints(maxWidth: 640),
1400 1401 1402
    );

  final BuildContext context;
1403
  late final ColorScheme _colors = Theme.of(context).colorScheme;
1404 1405

  @override
1406
  Color? get backgroundColor => _colors.surface;
1407 1408

  @override
1409
  Color? get surfaceTintColor => _colors.surfaceTint;
1410 1411

  @override
1412
  Color? get shadowColor => Colors.transparent;
1413 1414

  @override
1415
  Color? get dragHandleColor => _colors.onSurfaceVariant.withOpacity(0.4);
1416 1417

  @override
1418
  Size? get dragHandleSize => const Size(32, 4);
1419 1420 1421

  @override
  BoxConstraints? get constraints => const BoxConstraints(maxWidth: 640.0);
1422 1423 1424
}

// END GENERATED TOKEN PROPERTIES - BottomSheet