1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6library;
7
8import 'dart:math' as math;
9
10import 'package:flutter/gestures.dart';
11import 'package:flutter/rendering.dart';
12import 'package:flutter/widgets.dart';
13
14import 'bottom_sheet_theme.dart';
15import 'color_scheme.dart';
16import 'colors.dart';
17import 'constants.dart';
18import 'debug.dart';
19import 'material.dart';
20import 'material_localizations.dart';
21import 'motion.dart';
22import 'scaffold.dart';
23import 'theme.dart';
24
25const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
26const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
27const Curve _modalBottomSheetCurve = Easing.legacyDecelerate;
28const double _minFlingVelocity = 700.0;
29const double _closeProgressThreshold = 0.5;
30const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0;
31
32/// A callback for when the user begins dragging the bottom sheet.
33///
34/// Used by [BottomSheet.onDragStart].
35typedef BottomSheetDragStartHandler = void Function(DragStartDetails details);
36
37/// A callback for when the user stops dragging the bottom sheet.
38///
39/// Used by [BottomSheet.onDragEnd].
40typedef BottomSheetDragEndHandler =
41 void Function(DragEndDetails details, {required bool isClosing});
42
43/// A Material Design bottom sheet.
44///
45/// There are two kinds of bottom sheets in Material Design:
46///
47/// * _Persistent_. A persistent bottom sheet shows information that
48/// supplements the primary content of the app. A persistent bottom sheet
49/// remains visible even when the user interacts with other parts of the app.
50/// Persistent bottom sheets can be created and displayed with the
51/// [ScaffoldState.showBottomSheet] function or by specifying the
52/// [Scaffold.bottomSheet] constructor parameter.
53///
54/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
55/// prevents the user from interacting with the rest of the app. Modal bottom
56/// sheets can be created and displayed with the [showModalBottomSheet]
57/// function.
58///
59/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
60/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
61/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
62///
63/// See also:
64///
65/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
66/// non-modal "persistent" bottom sheets.
67/// * [showModalBottomSheet], which can be used to display a modal bottom
68/// sheet.
69/// * [BottomSheetThemeData], which can be used to customize the default
70/// bottom sheet property values.
71/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
72/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
73class BottomSheet extends StatefulWidget {
74 /// Creates a bottom sheet.
75 ///
76 /// Typically, bottom sheets are created implicitly by
77 /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
78 /// [showModalBottomSheet], for modal bottom sheets.
79 const BottomSheet({
80 super.key,
81 this.animationController,
82 this.enableDrag = true,
83 this.showDragHandle,
84 this.dragHandleColor,
85 this.dragHandleSize,
86 this.onDragStart,
87 this.onDragEnd,
88 this.backgroundColor,
89 this.shadowColor,
90 this.elevation,
91 this.shape,
92 this.clipBehavior,
93 this.constraints,
94 required this.onClosing,
95 required this.builder,
96 }) : assert(elevation == null || elevation >= 0.0);
97
98 /// The animation controller that controls the bottom sheet's entrance and
99 /// exit animations.
100 ///
101 /// The BottomSheet widget will manipulate the position of this animation, it
102 /// is not just a passive observer.
103 final AnimationController? animationController;
104
105 /// Called when the bottom sheet begins to close.
106 ///
107 /// A bottom sheet might be prevented from closing (e.g., by user
108 /// interaction) even after this callback is called. For this reason, this
109 /// callback might be call multiple times for a given bottom sheet.
110 final VoidCallback onClosing;
111
112 /// A builder for the contents of the sheet.
113 ///
114 /// The bottom sheet will wrap the widget produced by this builder in a
115 /// [Material] widget.
116 final WidgetBuilder builder;
117
118 /// If true, the bottom sheet can be dragged up and down and dismissed by
119 /// swiping downwards.
120 ///
121 /// If [showDragHandle] is true, this only applies to the content below the drag handle,
122 /// because the drag handle is always draggable.
123 ///
124 /// Default is true.
125 ///
126 /// If this is true, the [animationController] must not be null.
127 /// Use [BottomSheet.createAnimationController] to create one, or provide
128 /// another AnimationController.
129 final bool enableDrag;
130
131 /// Specifies whether a drag handle is shown.
132 ///
133 /// The drag handle appears at the top of the bottom sheet. The default color is
134 /// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
135 /// using [dragHandleColor]. The default size is `Size(32,4)` and can be customized
136 /// with [dragHandleSize].
137 ///
138 /// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
139 /// that is also null, defaults to false.
140 ///
141 /// If this is true, the [animationController] must not be null.
142 /// Use [BottomSheet.createAnimationController] to create one, or provide
143 /// another AnimationController.
144 final bool? showDragHandle;
145
146 /// The bottom sheet drag handle's color.
147 ///
148 /// Defaults to [BottomSheetThemeData.dragHandleColor].
149 /// If that is also null, defaults to [ColorScheme.onSurfaceVariant].
150 final Color? dragHandleColor;
151
152 /// Defaults to [BottomSheetThemeData.dragHandleSize].
153 /// If that is also null, defaults to Size(32, 4).
154 final Size? dragHandleSize;
155
156 /// Called when the user begins dragging the bottom sheet vertically, if
157 /// [enableDrag] is true.
158 ///
159 /// Would typically be used to change the bottom sheet animation curve so
160 /// that it tracks the user's finger accurately.
161 final BottomSheetDragStartHandler? onDragStart;
162
163 /// Called when the user stops dragging the bottom sheet, if [enableDrag]
164 /// is true.
165 ///
166 /// Would typically be used to reset the bottom sheet animation curve, so
167 /// that it animates non-linearly. Called before [onClosing] if the bottom
168 /// sheet is closing.
169 final BottomSheetDragEndHandler? onDragEnd;
170
171 /// The bottom sheet's background color.
172 ///
173 /// Defines the bottom sheet's [Material.color].
174 ///
175 /// Defaults to null and falls back to [Material]'s default.
176 final Color? backgroundColor;
177
178 /// The color of the shadow below the sheet.
179 ///
180 /// If this property is null, then [BottomSheetThemeData.shadowColor] of
181 /// [ThemeData.bottomSheetTheme] is used. If that is also null, the default value
182 /// is transparent.
183 ///
184 /// See also:
185 ///
186 /// * [elevation], which defines the size of the shadow below the sheet.
187 /// * [shape], which defines the shape of the sheet and its shadow.
188 final Color? shadowColor;
189
190 /// The z-coordinate at which to place this material relative to its parent.
191 ///
192 /// This controls the size of the shadow below the material.
193 ///
194 /// Defaults to 0. The value is non-negative.
195 final double? elevation;
196
197 /// The shape of the bottom sheet.
198 ///
199 /// Defines the bottom sheet's [Material.shape].
200 ///
201 /// Defaults to null and falls back to [Material]'s default.
202 final ShapeBorder? shape;
203
204 /// {@macro flutter.material.Material.clipBehavior}
205 ///
206 /// Defines the bottom sheet's [Material.clipBehavior].
207 ///
208 /// Use this property to enable clipping of content when the bottom sheet has
209 /// a custom [shape] and the content can extend past this shape. For example,
210 /// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
211 /// top.
212 ///
213 /// If this property is null then [BottomSheetThemeData.clipBehavior] of
214 /// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior
215 /// will be [Clip.none].
216 final Clip? clipBehavior;
217
218 /// Defines minimum and maximum sizes for a [BottomSheet].
219 ///
220 /// If null, then the ambient [ThemeData.bottomSheetTheme]'s
221 /// [BottomSheetThemeData.constraints] will be used. If that
222 /// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
223 /// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
224 /// the bottom sheet's size will be constrained by its parent
225 /// (usually a [Scaffold]). In this case, consider limiting the width by
226 /// setting smaller constraints for large screens.
227 ///
228 /// If constraints are specified (either in this property or in the
229 /// theme), the bottom sheet will be aligned to the bottom-center of
230 /// the available space. Otherwise, no alignment is applied.
231 final BoxConstraints? constraints;
232
233 @override
234 State<BottomSheet> createState() => _BottomSheetState();
235
236 /// Creates an [AnimationController] suitable for a
237 /// [BottomSheet.animationController].
238 ///
239 /// This API is available as a convenience for a Material compliant bottom sheet
240 /// animation. If alternative animation durations are required, a different
241 /// animation controller could be provided.
242 static AnimationController createAnimationController(
243 TickerProvider vsync, {
244 AnimationStyle? sheetAnimationStyle,
245 }) {
246 return AnimationController(
247 duration: sheetAnimationStyle?.duration ?? _bottomSheetEnterDuration,
248 reverseDuration: sheetAnimationStyle?.reverseDuration ?? _bottomSheetExitDuration,
249 debugLabel: 'BottomSheet',
250 vsync: vsync,
251 );
252 }
253}
254
255class _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 return renderBox.size.height;
261 }
262
263 bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse;
264
265 Set<WidgetState> dragHandleStates = <WidgetState>{};
266
267 void _handleDragStart(DragStartDetails details) {
268 setState(() {
269 dragHandleStates.add(WidgetState.dragged);
270 });
271 widget.onDragStart?.call(details);
272 }
273
274 void _handleDragUpdate(DragUpdateDetails details) {
275 assert(
276 (widget.enableDrag || (widget.showDragHandle ?? false)) && widget.animationController != null,
277 "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
278 "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
279 );
280 if (_dismissUnderway) {
281 return;
282 }
283 widget.animationController!.value -= details.primaryDelta! / _childHeight;
284 }
285
286 void _handleDragEnd(DragEndDetails details) {
287 assert(
288 (widget.enableDrag || (widget.showDragHandle ?? false)) && widget.animationController != null,
289 "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
290 "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
291 );
292 if (_dismissUnderway) {
293 return;
294 }
295 setState(() {
296 dragHandleStates.remove(WidgetState.dragged);
297 });
298 bool isClosing = false;
299 if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
300 final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
301 if (widget.animationController!.value > 0.0) {
302 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 widget.onDragEnd?.call(details, isClosing: isClosing);
317
318 if (isClosing) {
319 widget.onClosing();
320 }
321 }
322
323 bool extentChanged(DraggableScrollableNotification notification) {
324 if (notification.extent == notification.minExtent && notification.shouldCloseOnMinExtent) {
325 widget.onClosing();
326 }
327 return false;
328 }
329
330 void _handleDragHandleHover(bool hovering) {
331 if (hovering != dragHandleStates.contains(WidgetState.hovered)) {
332 setState(() {
333 if (hovering) {
334 dragHandleStates.add(WidgetState.hovered);
335 } else {
336 dragHandleStates.remove(WidgetState.hovered);
337 }
338 });
339 }
340 }
341
342 @override
343 Widget build(BuildContext context) {
344 final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
345 final bool useMaterial3 = Theme.of(context).useMaterial3;
346 final BottomSheetThemeData defaults = useMaterial3
347 ? _BottomSheetDefaultsM3(context)
348 : const BottomSheetThemeData();
349 final BoxConstraints? constraints =
350 widget.constraints ?? bottomSheetTheme.constraints ?? defaults.constraints;
351 final Color? color =
352 widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor;
353 final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
354 final Color? shadowColor =
355 widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor;
356 final double elevation =
357 widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0;
358 final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
359 final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
360 final bool showDragHandle =
361 widget.showDragHandle ?? (widget.enableDrag && (bottomSheetTheme.showDragHandle ?? false));
362
363 Widget? dragHandle;
364 if (showDragHandle) {
365 dragHandle = _DragHandle(
366 onSemanticsTap: widget.onClosing,
367 handleHover: _handleDragHandleHover,
368 states: dragHandleStates,
369 dragHandleColor: widget.dragHandleColor,
370 dragHandleSize: widget.dragHandleSize,
371 );
372 // Only add [_BottomSheetGestureDetector] to the drag handle when the rest of the
373 // bottom sheet is not draggable. If the whole bottom sheet is draggable,
374 // no need to add it.
375 if (!widget.enableDrag) {
376 dragHandle = _BottomSheetGestureDetector(
377 onVerticalDragStart: _handleDragStart,
378 onVerticalDragUpdate: _handleDragUpdate,
379 onVerticalDragEnd: _handleDragEnd,
380 child: dragHandle,
381 );
382 }
383 }
384
385 Widget bottomSheet = Material(
386 key: _childKey,
387 color: color,
388 elevation: elevation,
389 surfaceTintColor: surfaceTintColor,
390 shadowColor: shadowColor,
391 shape: shape,
392 clipBehavior: clipBehavior,
393 child: NotificationListener<DraggableScrollableNotification>(
394 onNotification: extentChanged,
395 child: !showDragHandle
396 ? widget.builder(context)
397 : Stack(
398 alignment: Alignment.topCenter,
399 children: <Widget>[
400 dragHandle!,
401 Padding(
402 padding: const EdgeInsets.only(top: kMinInteractiveDimension),
403 child: widget.builder(context),
404 ),
405 ],
406 ),
407 ),
408 );
409
410 if (constraints != null) {
411 bottomSheet = Align(
412 alignment: Alignment.bottomCenter,
413 heightFactor: 1.0,
414 child: ConstrainedBox(constraints: constraints, child: bottomSheet),
415 );
416 }
417
418 return !widget.enableDrag
419 ? bottomSheet
420 : _BottomSheetGestureDetector(
421 onVerticalDragStart: _handleDragStart,
422 onVerticalDragUpdate: _handleDragUpdate,
423 onVerticalDragEnd: _handleDragEnd,
424 child: bottomSheet,
425 );
426 }
427}
428
429// PERSISTENT BOTTOM SHEETS
430
431// See scaffold.dart
432
433class _DragHandle extends StatelessWidget {
434 const _DragHandle({
435 required this.onSemanticsTap,
436 required this.handleHover,
437 required this.states,
438 this.dragHandleColor,
439 this.dragHandleSize,
440 });
441
442 final VoidCallback? onSemanticsTap;
443 final ValueChanged<bool> handleHover;
444 final Set<WidgetState> states;
445 final Color? dragHandleColor;
446 final Size? dragHandleSize;
447
448 @override
449 Widget build(BuildContext context) {
450 final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
451 final BottomSheetThemeData m3Defaults = _BottomSheetDefaultsM3(context);
452 final Size handleSize =
453 dragHandleSize ?? bottomSheetTheme.dragHandleSize ?? m3Defaults.dragHandleSize!;
454
455 return MouseRegion(
456 onEnter: (PointerEnterEvent event) => handleHover(true),
457 onExit: (PointerExitEvent event) => handleHover(false),
458 child: Semantics(
459 label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
460 container: true,
461 button: true,
462 onTap: onSemanticsTap,
463 child: SizedBox(
464 width: math.max(handleSize.width, kMinInteractiveDimension),
465 height: math.max(handleSize.height, kMinInteractiveDimension),
466 child: Center(
467 child: Container(
468 height: handleSize.height,
469 width: handleSize.width,
470 decoration: BoxDecoration(
471 borderRadius: BorderRadius.circular(handleSize.height / 2),
472 color:
473 WidgetStateProperty.resolveAs<Color?>(dragHandleColor, states) ??
474 WidgetStateProperty.resolveAs<Color?>(
475 bottomSheetTheme.dragHandleColor,
476 states,
477 ) ??
478 m3Defaults.dragHandleColor,
479 ),
480 ),
481 ),
482 ),
483 ),
484 );
485 }
486}
487
488class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
489 const _BottomSheetLayoutWithSizeListener({
490 required this.onChildSizeChanged,
491 required this.animationValue,
492 required this.isScrollControlled,
493 required this.scrollControlDisabledMaxHeightRatio,
494 super.child,
495 });
496
497 final ValueChanged<Size> onChildSizeChanged;
498 final double animationValue;
499 final bool isScrollControlled;
500 final double scrollControlDisabledMaxHeightRatio;
501
502 @override
503 _RenderBottomSheetLayoutWithSizeListener createRenderObject(BuildContext context) {
504 return _RenderBottomSheetLayoutWithSizeListener(
505 onChildSizeChanged: onChildSizeChanged,
506 animationValue: animationValue,
507 isScrollControlled: isScrollControlled,
508 scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
509 );
510 }
511
512 @override
513 void updateRenderObject(
514 BuildContext context,
515 _RenderBottomSheetLayoutWithSizeListener renderObject,
516 ) {
517 renderObject.onChildSizeChanged = onChildSizeChanged;
518 renderObject.animationValue = animationValue;
519 renderObject.isScrollControlled = isScrollControlled;
520 renderObject.scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio;
521 }
522}
523
524class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
525 _RenderBottomSheetLayoutWithSizeListener({
526 RenderBox? child,
527 required ValueChanged<Size> onChildSizeChanged,
528 required double animationValue,
529 required bool isScrollControlled,
530 required double scrollControlDisabledMaxHeightRatio,
531 }) : _onChildSizeChanged = onChildSizeChanged,
532 _animationValue = animationValue,
533 _isScrollControlled = isScrollControlled,
534 _scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio,
535 super(child);
536
537 Size _lastSize = Size.zero;
538
539 ValueChanged<Size> get onChildSizeChanged => _onChildSizeChanged;
540 ValueChanged<Size> _onChildSizeChanged;
541 set onChildSizeChanged(ValueChanged<Size> newCallback) {
542 if (_onChildSizeChanged == newCallback) {
543 return;
544 }
545
546 _onChildSizeChanged = newCallback;
547 markNeedsLayout();
548 }
549
550 double get animationValue => _animationValue;
551 double _animationValue;
552 set animationValue(double newValue) {
553 if (_animationValue == newValue) {
554 return;
555 }
556
557 _animationValue = newValue;
558 markNeedsLayout();
559 }
560
561 bool get isScrollControlled => _isScrollControlled;
562 bool _isScrollControlled;
563 set isScrollControlled(bool newValue) {
564 if (_isScrollControlled == newValue) {
565 return;
566 }
567
568 _isScrollControlled = newValue;
569 markNeedsLayout();
570 }
571
572 double get scrollControlDisabledMaxHeightRatio => _scrollControlDisabledMaxHeightRatio;
573 double _scrollControlDisabledMaxHeightRatio;
574 set scrollControlDisabledMaxHeightRatio(double newValue) {
575 if (_scrollControlDisabledMaxHeightRatio == newValue) {
576 return;
577 }
578
579 _scrollControlDisabledMaxHeightRatio = newValue;
580 markNeedsLayout();
581 }
582
583 @override
584 double computeMinIntrinsicWidth(double height) => 0.0;
585
586 @override
587 double computeMaxIntrinsicWidth(double height) => 0.0;
588
589 @override
590 double computeMinIntrinsicHeight(double width) => 0.0;
591
592 @override
593 double computeMaxIntrinsicHeight(double width) => 0.0;
594
595 @override
596 Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
597
598 @override
599 double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
600 final RenderBox? child = this.child;
601 if (child == null) {
602 return null;
603 }
604 final BoxConstraints childConstraints = _getConstraintsForChild(constraints);
605 final double? result = child.getDryBaseline(childConstraints, baseline);
606 if (result == null) {
607 return null;
608 }
609 final Size childSize = childConstraints.isTight
610 ? childConstraints.smallest
611 : child.getDryLayout(childConstraints);
612 return result + _getPositionForChild(constraints.biggest, childSize).dy;
613 }
614
615 BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
616 return BoxConstraints(
617 minWidth: constraints.maxWidth,
618 maxWidth: constraints.maxWidth,
619 maxHeight: isScrollControlled
620 ? constraints.maxHeight
621 : constraints.maxHeight * scrollControlDisabledMaxHeightRatio,
622 );
623 }
624
625 Offset _getPositionForChild(Size size, Size childSize) {
626 return Offset(0.0, size.height - childSize.height * animationValue);
627 }
628
629 @override
630 void performLayout() {
631 size = constraints.biggest;
632 final RenderBox? child = this.child;
633 if (child == null) {
634 return;
635 }
636
637 final BoxConstraints childConstraints = _getConstraintsForChild(constraints);
638 assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
639 child.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
640 final BoxParentData childParentData = child.parentData! as BoxParentData;
641 final Size childSize = childConstraints.isTight ? childConstraints.smallest : child.size;
642 childParentData.offset = _getPositionForChild(size, childSize);
643
644 if (_lastSize != childSize) {
645 _lastSize = childSize;
646 _onChildSizeChanged.call(_lastSize);
647 }
648 }
649}
650
651class _ModalBottomSheet<T> extends StatefulWidget {
652 const _ModalBottomSheet({
653 super.key,
654 required this.route,
655 this.backgroundColor,
656 this.elevation,
657 this.shape,
658 this.clipBehavior,
659 this.constraints,
660 this.isScrollControlled = false,
661 this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
662 this.enableDrag = true,
663 this.showDragHandle = false,
664 });
665
666 final ModalBottomSheetRoute<T> route;
667 final bool isScrollControlled;
668 final double scrollControlDisabledMaxHeightRatio;
669 final Color? backgroundColor;
670 final double? elevation;
671 final ShapeBorder? shape;
672 final Clip? clipBehavior;
673 final BoxConstraints? constraints;
674 final bool enableDrag;
675 final bool showDragHandle;
676
677 @override
678 _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
679}
680
681class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
682 ParametricCurve<double> animationCurve = _modalBottomSheetCurve;
683
684 String _getRouteLabel(MaterialLocalizations localizations) {
685 switch (Theme.of(context).platform) {
686 case TargetPlatform.iOS:
687 case TargetPlatform.macOS:
688 return '';
689 case TargetPlatform.android:
690 case TargetPlatform.fuchsia:
691 case TargetPlatform.linux:
692 case TargetPlatform.windows:
693 return localizations.dialogLabel;
694 }
695 }
696
697 EdgeInsets _getNewClipDetails(Size topLayerSize) {
698 return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
699 }
700
701 void handleDragStart(DragStartDetails details) {
702 // Allow the bottom sheet to track the user's finger accurately.
703 animationCurve = Curves.linear;
704 }
705
706 void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
707 // Allow the bottom sheet to animate smoothly from its current position.
708 animationCurve = Split(widget.route.animation!.value, endCurve: _modalBottomSheetCurve);
709 }
710
711 @override
712 Widget build(BuildContext context) {
713 assert(debugCheckHasMediaQuery(context));
714 assert(debugCheckHasMaterialLocalizations(context));
715 final MaterialLocalizations localizations = MaterialLocalizations.of(context);
716 final String routeLabel = _getRouteLabel(localizations);
717
718 return AnimatedBuilder(
719 animation: widget.route.animation!,
720 child: BottomSheet(
721 animationController: widget.route._animationController,
722 onClosing: () {
723 if (widget.route.isCurrent) {
724 Navigator.pop(context);
725 }
726 },
727 builder: widget.route.builder,
728 backgroundColor: widget.backgroundColor,
729 elevation: widget.elevation,
730 shape: widget.shape,
731 clipBehavior: widget.clipBehavior,
732 constraints: widget.constraints,
733 enableDrag: widget.enableDrag,
734 showDragHandle: widget.showDragHandle,
735 onDragStart: handleDragStart,
736 onDragEnd: handleDragEnd,
737 ),
738 builder: (BuildContext context, Widget? child) {
739 final double animationValue = animationCurve.transform(widget.route.animation!.value);
740 return Semantics(
741 scopesRoute: true,
742 namesRoute: true,
743 label: routeLabel,
744 explicitChildNodes: true,
745 child: ClipRect(
746 child: _BottomSheetLayoutWithSizeListener(
747 onChildSizeChanged: (Size size) {
748 widget.route._didChangeBarrierSemanticsClip(_getNewClipDetails(size));
749 },
750 animationValue: animationValue,
751 isScrollControlled: widget.isScrollControlled,
752 scrollControlDisabledMaxHeightRatio: widget.scrollControlDisabledMaxHeightRatio,
753 child: child,
754 ),
755 ),
756 );
757 },
758 );
759 }
760}
761
762/// A route that represents a Material Design modal bottom sheet.
763///
764/// {@template flutter.material.ModalBottomSheetRoute}
765/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
766/// the user from interacting with the rest of the app.
767///
768/// A closely related widget is a persistent bottom sheet, which shows
769/// information that supplements the primary content of the app without
770/// preventing the user from interacting with the app. Persistent bottom sheets
771/// can be created and displayed with the [showBottomSheet] function or the
772/// [ScaffoldState.showBottomSheet] method.
773///
774/// The [isScrollControlled] parameter specifies whether this is a route for
775/// a bottom sheet that will utilize [DraggableScrollableSheet]. Consider
776/// setting this parameter to true if this bottom sheet has
777/// a scrollable child, such as a [ListView] or a [GridView],
778/// to have the bottom sheet be draggable.
779///
780/// The [isDismissible] parameter specifies whether the bottom sheet will be
781/// dismissed when user taps on the scrim.
782///
783/// The [enableDrag] parameter specifies whether the bottom sheet can be
784/// dragged up and down and dismissed by swiping downwards.
785///
786/// The [useSafeArea] parameter specifies whether the sheet will avoid system
787/// intrusions on the top, left, and right. If false, no [SafeArea] is added;
788/// and [MediaQuery.removePadding] is applied to the top,
789/// so that system intrusions at the top will not be avoided by a [SafeArea]
790/// inside the bottom sheet either.
791/// Defaults to false.
792///
793/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
794/// [constraints] and [transitionAnimationController]
795/// parameters can be passed in to customize the appearance and behavior of
796/// modal bottom sheets (see the documentation for these on [BottomSheet]
797/// for more details).
798///
799/// The [transitionAnimationController] controls the bottom sheet's entrance and
800/// exit animations. It's up to the owner of the controller to call
801/// [AnimationController.dispose] when the controller is no longer needed.
802///
803/// The optional `settings` parameter sets the [RouteSettings] of the modal bottom sheet
804/// sheet. This is particularly useful in the case that a user wants to observe
805/// [PopupRoute]s within a [NavigatorObserver].
806/// {@endtemplate}
807///
808/// {@macro flutter.widgets.RawDialogRoute}
809///
810/// See also:
811///
812/// * [showModalBottomSheet], which is a way to display a ModalBottomSheetRoute.
813/// * [BottomSheet], which becomes the parent of the widget returned by the
814/// function passed as the `builder` argument to [showModalBottomSheet].
815/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
816/// non-modal bottom sheets.
817/// * [DraggableScrollableSheet], creates a bottom sheet that grows
818/// and then becomes scrollable once it reaches its maximum size.
819/// * [DisplayFeatureSubScreen], which documents the specifics of how
820/// [DisplayFeature]s can split the screen into sub-screens.
821/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
822/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
823class ModalBottomSheetRoute<T> extends PopupRoute<T> {
824 /// A modal bottom sheet route.
825 ModalBottomSheetRoute({
826 required this.builder,
827 this.capturedThemes,
828 this.barrierLabel,
829 this.barrierOnTapHint,
830 this.backgroundColor,
831 this.elevation,
832 this.shape,
833 this.clipBehavior,
834 this.constraints,
835 this.modalBarrierColor,
836 this.isDismissible = true,
837 this.enableDrag = true,
838 this.showDragHandle,
839 required this.isScrollControlled,
840 this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
841 super.settings,
842 super.requestFocus,
843 this.transitionAnimationController,
844 this.anchorPoint,
845 this.useSafeArea = false,
846 this.sheetAnimationStyle,
847 });
848
849 /// A builder for the contents of the sheet.
850 ///
851 /// The bottom sheet will wrap the widget produced by this builder in a
852 /// [Material] widget.
853 final WidgetBuilder builder;
854
855 /// Stores a list of captured [InheritedTheme]s that are wrapped around the
856 /// bottom sheet.
857 ///
858 /// Consider setting this attribute when the [ModalBottomSheetRoute]
859 /// is created through [Navigator.push] and its friends.
860 final CapturedThemes? capturedThemes;
861
862 /// Specifies whether this is a route for a bottom sheet that will utilize
863 /// [DraggableScrollableSheet].
864 ///
865 /// Consider setting this parameter to true if this bottom sheet has
866 /// a scrollable child, such as a [ListView] or a [GridView],
867 /// to have the bottom sheet be draggable.
868 final bool isScrollControlled;
869
870 /// The max height constraint ratio for the bottom sheet
871 /// when [isScrollControlled] is set to false,
872 /// no ratio will be applied when [isScrollControlled] is set to true.
873 ///
874 /// Defaults to 9 / 16.
875 final double scrollControlDisabledMaxHeightRatio;
876
877 /// The bottom sheet's background color.
878 ///
879 /// Defines the bottom sheet's [Material.color].
880 ///
881 /// If this property is not provided, it falls back to [Material]'s default.
882 final Color? backgroundColor;
883
884 /// The z-coordinate at which to place this material relative to its parent.
885 ///
886 /// This controls the size of the shadow below the material.
887 ///
888 /// Defaults to 0, must not be negative.
889 final double? elevation;
890
891 /// The shape of the bottom sheet.
892 ///
893 /// Defines the bottom sheet's [Material.shape].
894 ///
895 /// If this property is not provided, it falls back to [Material]'s default.
896 final ShapeBorder? shape;
897
898 /// {@macro flutter.material.Material.clipBehavior}
899 ///
900 /// Defines the bottom sheet's [Material.clipBehavior].
901 ///
902 /// Use this property to enable clipping of content when the bottom sheet has
903 /// a custom [shape] and the content can extend past this shape. For example,
904 /// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
905 /// top.
906 ///
907 /// If this property is null, the [BottomSheetThemeData.clipBehavior] of
908 /// [ThemeData.bottomSheetTheme] is used. If that's null, the behavior defaults to [Clip.none]
909 /// will be [Clip.none].
910 final Clip? clipBehavior;
911
912 /// Defines minimum and maximum sizes for a [BottomSheet].
913 ///
914 /// If null, the ambient [ThemeData.bottomSheetTheme]'s
915 /// [BottomSheetThemeData.constraints] will be used. If that
916 /// is null and [ThemeData.useMaterial3] is true, then the bottom sheet
917 /// will have a max width of 640dp. If [ThemeData.useMaterial3] is false, then
918 /// the bottom sheet's size will be constrained by its parent
919 /// (usually a [Scaffold]). In this case, consider limiting the width by
920 /// setting smaller constraints for large screens.
921 ///
922 /// If constraints are specified (either in this property or in the
923 /// theme), the bottom sheet will be aligned to the bottom-center of
924 /// the available space. Otherwise, no alignment is applied.
925 final BoxConstraints? constraints;
926
927 /// Specifies the color of the modal barrier that darkens everything below the
928 /// bottom sheet.
929 ///
930 /// Defaults to `Colors.black54` if not provided.
931 final Color? modalBarrierColor;
932
933 /// Specifies whether the bottom sheet will be dismissed
934 /// when user taps on the scrim.
935 ///
936 /// If true, the bottom sheet will be dismissed when user taps on the scrim.
937 ///
938 /// Defaults to true.
939 final bool isDismissible;
940
941 /// Specifies whether the bottom sheet can be dragged up and down
942 /// and dismissed by swiping downwards.
943 ///
944 /// If true, the bottom sheet can be dragged up and down and dismissed by
945 /// swiping downwards.
946 ///
947 /// This applies to the content below the drag handle, if showDragHandle is true.
948 ///
949 /// Defaults is true.
950 final bool enableDrag;
951
952 /// Specifies whether a drag handle is shown.
953 ///
954 /// The drag handle appears at the top of the bottom sheet. The default color is
955 /// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
956 /// using dragHandleColor. The default size is `Size(32,4)` and can be customized
957 /// with dragHandleSize.
958 ///
959 /// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
960 /// that is also null, defaults to false.
961 final bool? showDragHandle;
962
963 /// The animation controller that controls the bottom sheet's entrance and
964 /// exit animations.
965 ///
966 /// The BottomSheet widget will manipulate the position of this animation, it
967 /// is not just a passive observer.
968 final AnimationController? transitionAnimationController;
969
970 /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
971 final Offset? anchorPoint;
972
973 /// Whether to avoid system intrusions on the top, left, and right.
974 ///
975 /// If true, a [SafeArea] is inserted to keep the bottom sheet away from
976 /// system intrusions at the top, left, and right sides of the screen.
977 ///
978 /// If false, the bottom sheet will extend through any system intrusions
979 /// at the top, left, and right.
980 ///
981 /// If false, then moreover [MediaQuery.removePadding] will be used
982 /// to remove top padding, so that a [SafeArea] widget inside the bottom
983 /// sheet will have no effect at the top edge. If this is undesired, consider
984 /// setting [useSafeArea] to true. Alternatively, wrap the [SafeArea] in a
985 /// [MediaQuery] that restates an ambient [MediaQueryData] from outside [builder].
986 ///
987 /// In either case, the bottom sheet extends all the way to the bottom of
988 /// the screen, including any system intrusions.
989 ///
990 /// The default is false.
991 final bool useSafeArea;
992
993 /// Used to override the modal bottom sheet animation duration and reverse
994 /// animation duration.
995 ///
996 /// If [AnimationStyle.duration] is provided, it will be used to override
997 /// the modal bottom sheet animation duration in the underlying
998 /// [BottomSheet.createAnimationController].
999 ///
1000 /// If [AnimationStyle.reverseDuration] is provided, it will be used to
1001 /// override the modal bottom sheet reverse animation duration in the
1002 /// underlying [BottomSheet.createAnimationController].
1003 ///
1004 /// To disable the modal bottom sheet animation, use [AnimationStyle.noAnimation].
1005 final AnimationStyle? sheetAnimationStyle;
1006
1007 /// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
1008 /// The semantic hint text that informs users what will happen if they
1009 /// tap on the widget. Announced in the format of 'Double tap to ...'.
1010 ///
1011 /// If the field is null, the default hint will be used, which results in
1012 /// announcement of 'Double tap to activate'.
1013 /// {@endtemplate}
1014 ///
1015 /// See also:
1016 ///
1017 /// * [barrierDismissible], which controls the behavior of the barrier when
1018 /// tapped.
1019 /// * [ModalBarrier], which uses this field as onTapHint when it has an onTap action.
1020 final String? barrierOnTapHint;
1021
1022 final ValueNotifier<EdgeInsets> _clipDetailsNotifier = ValueNotifier<EdgeInsets>(EdgeInsets.zero);
1023
1024 @override
1025 void dispose() {
1026 _clipDetailsNotifier.dispose();
1027 super.dispose();
1028 }
1029
1030 /// Updates the details regarding how the [SemanticsNode.rect] (focus) of
1031 /// the barrier for this [ModalBottomSheetRoute] should be clipped.
1032 ///
1033 /// Returns true if the clipDetails did change and false otherwise.
1034 bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
1035 if (_clipDetailsNotifier.value == newClipDetails) {
1036 return false;
1037 }
1038 _clipDetailsNotifier.value = newClipDetails;
1039 return true;
1040 }
1041
1042 @override
1043 Duration get transitionDuration =>
1044 transitionAnimationController?.duration ??
1045 sheetAnimationStyle?.duration ??
1046 _bottomSheetEnterDuration;
1047
1048 @override
1049 Duration get reverseTransitionDuration =>
1050 transitionAnimationController?.reverseDuration ??
1051 transitionAnimationController?.duration ??
1052 sheetAnimationStyle?.reverseDuration ??
1053 _bottomSheetExitDuration;
1054
1055 @override
1056 bool get barrierDismissible => isDismissible;
1057
1058 @override
1059 final String? barrierLabel;
1060
1061 @override
1062 Color get barrierColor => modalBarrierColor ?? Colors.black54;
1063
1064 AnimationController? _animationController;
1065
1066 @override
1067 AnimationController createAnimationController() {
1068 assert(_animationController == null);
1069 if (transitionAnimationController != null) {
1070 _animationController = transitionAnimationController;
1071 willDisposeAnimationController = false;
1072 } else {
1073 _animationController = BottomSheet.createAnimationController(
1074 navigator!,
1075 sheetAnimationStyle: sheetAnimationStyle,
1076 );
1077 }
1078 return _animationController!;
1079 }
1080
1081 @override
1082 Widget buildPage(
1083 BuildContext context,
1084 Animation<double> animation,
1085 Animation<double> secondaryAnimation,
1086 ) {
1087 final Widget content = DisplayFeatureSubScreen(
1088 anchorPoint: anchorPoint,
1089 child: Builder(
1090 builder: (BuildContext context) {
1091 final BottomSheetThemeData sheetTheme = Theme.of(context).bottomSheetTheme;
1092 final BottomSheetThemeData defaults = Theme.of(context).useMaterial3
1093 ? _BottomSheetDefaultsM3(context)
1094 : const BottomSheetThemeData();
1095 return _ModalBottomSheet<T>(
1096 route: this,
1097 backgroundColor:
1098 backgroundColor ??
1099 sheetTheme.modalBackgroundColor ??
1100 sheetTheme.backgroundColor ??
1101 defaults.backgroundColor,
1102 elevation:
1103 elevation ??
1104 sheetTheme.modalElevation ??
1105 sheetTheme.elevation ??
1106 defaults.modalElevation,
1107 shape: shape,
1108 clipBehavior: clipBehavior,
1109 constraints: constraints,
1110 isScrollControlled: isScrollControlled,
1111 scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
1112 enableDrag: enableDrag,
1113 showDragHandle: showDragHandle ?? (enableDrag && (sheetTheme.showDragHandle ?? false)),
1114 );
1115 },
1116 ),
1117 );
1118
1119 final Widget bottomSheet = useSafeArea
1120 ? SafeArea(bottom: false, child: content)
1121 : MediaQuery.removePadding(context: context, removeTop: true, child: content);
1122
1123 return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
1124 }
1125
1126 @override
1127 Widget buildModalBarrier() {
1128 if (barrierColor.a != 0 && !offstage) {
1129 // changedInternalState is called if barrierColor or offstage updates
1130 assert(barrierColor != barrierColor.withValues(alpha: 0.0));
1131 final Animation<Color?> color = animation!.drive(
1132 ColorTween(
1133 begin: barrierColor.withValues(alpha: 0.0),
1134 end: barrierColor, // changedInternalState is called if barrierColor updates
1135 ).chain(
1136 CurveTween(curve: barrierCurve),
1137 ), // changedInternalState is called if barrierCurve updates
1138 );
1139 return AnimatedModalBarrier(
1140 color: color,
1141 dismissible:
1142 barrierDismissible, // changedInternalState is called if barrierDismissible updates
1143 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1144 barrierSemanticsDismissible: semanticsDismissible,
1145 clipDetailsNotifier: _clipDetailsNotifier,
1146 semanticsOnTapHint: barrierOnTapHint,
1147 );
1148 } else {
1149 return ModalBarrier(
1150 dismissible:
1151 barrierDismissible, // changedInternalState is called if barrierDismissible updates
1152 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1153 barrierSemanticsDismissible: semanticsDismissible,
1154 clipDetailsNotifier: _clipDetailsNotifier,
1155 semanticsOnTapHint: barrierOnTapHint,
1156 );
1157 }
1158 }
1159}
1160
1161/// Shows a modal Material Design bottom sheet.
1162///
1163/// {@macro flutter.material.ModalBottomSheetRoute}
1164///
1165/// {@macro flutter.widgets.RawDialogRoute}
1166///
1167/// The `context` argument is used to look up the [Navigator] and [Theme] for
1168/// the bottom sheet. It is only used when the method is called. Its
1169/// corresponding widget can be safely removed from the tree before the bottom
1170/// sheet is closed.
1171///
1172/// The `useRootNavigator` parameter ensures that the root navigator is used to
1173/// display the [BottomSheet] when set to `true`. This is useful in the case
1174/// that a modal [BottomSheet] needs to be displayed above all other content
1175/// but the caller is inside another [Navigator].
1176///
1177/// Returns a `Future` that resolves to the value (if any) that was passed to
1178/// [Navigator.pop] when the modal bottom sheet was closed.
1179///
1180/// The 'barrierLabel' parameter can be used to set a custom barrier label.
1181/// Will default to [MaterialLocalizations.modalBarrierDismissLabel] of context
1182/// if not set.
1183///
1184/// {@tool dartpad}
1185/// This example demonstrates how to use [showModalBottomSheet] to display a
1186/// bottom sheet that obscures the content behind it when a user taps a button.
1187/// It also demonstrates how to close the bottom sheet using the [Navigator]
1188/// when a user taps on a button inside the bottom sheet.
1189///
1190/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart **
1191/// {@end-tool}
1192///
1193/// {@tool dartpad}
1194/// This sample shows the creation of [showModalBottomSheet], as described in:
1195/// https://m3.material.io/components/bottom-sheets/overview
1196///
1197/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart **
1198/// {@end-tool}
1199///
1200/// The [sheetAnimationStyle] parameter is used to override the modal bottom sheet
1201/// animation duration and reverse animation duration.
1202///
1203/// The [requestFocus] parameter is used to specify whether the bottom sheet should
1204/// request focus when shown.
1205/// {@macro flutter.widgets.navigator.Route.requestFocus}
1206///
1207/// If [AnimationStyle.duration] is provided, it will be used to override
1208/// the modal bottom sheet animation duration in the underlying
1209/// [BottomSheet.createAnimationController].
1210///
1211/// If [AnimationStyle.reverseDuration] is provided, it will be used to
1212/// override the modal bottom sheet reverse animation duration in the
1213/// underlying [BottomSheet.createAnimationController].
1214///
1215/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
1216///
1217/// {@tool dartpad}
1218/// This sample showcases how to override the [showModalBottomSheet] animation
1219/// duration and reverse animation duration using [AnimationStyle].
1220///
1221/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart **
1222/// {@end-tool}
1223///
1224/// See also:
1225///
1226/// * [BottomSheet], which becomes the parent of the widget returned by the
1227/// function passed as the `builder` argument to [showModalBottomSheet].
1228/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
1229/// non-modal bottom sheets.
1230/// * [DraggableScrollableSheet], creates a bottom sheet that grows
1231/// and then becomes scrollable once it reaches its maximum size.
1232/// * [DisplayFeatureSubScreen], which documents the specifics of how
1233/// [DisplayFeature]s can split the screen into sub-screens.
1234/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
1235/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
1236/// * [AnimationStyle], which is used to override the modal bottom sheet
1237/// animation duration and reverse animation duration.
1238Future<T?> showModalBottomSheet<T>({
1239 required BuildContext context,
1240 required WidgetBuilder builder,
1241 Color? backgroundColor,
1242 String? barrierLabel,
1243 double? elevation,
1244 ShapeBorder? shape,
1245 Clip? clipBehavior,
1246 BoxConstraints? constraints,
1247 Color? barrierColor,
1248 bool isScrollControlled = false,
1249 double scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio,
1250 bool useRootNavigator = false,
1251 bool isDismissible = true,
1252 bool enableDrag = true,
1253 bool? showDragHandle,
1254 bool useSafeArea = false,
1255 RouteSettings? routeSettings,
1256 AnimationController? transitionAnimationController,
1257 Offset? anchorPoint,
1258 AnimationStyle? sheetAnimationStyle,
1259 bool? requestFocus,
1260}) {
1261 assert(debugCheckHasMediaQuery(context));
1262 assert(debugCheckHasMaterialLocalizations(context));
1263
1264 final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
1265 final MaterialLocalizations localizations = MaterialLocalizations.of(context);
1266 return navigator.push(
1267 ModalBottomSheetRoute<T>(
1268 builder: builder,
1269 capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
1270 isScrollControlled: isScrollControlled,
1271 scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
1272 barrierLabel: barrierLabel ?? localizations.scrimLabel,
1273 barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel),
1274 backgroundColor: backgroundColor,
1275 elevation: elevation,
1276 shape: shape,
1277 clipBehavior: clipBehavior,
1278 constraints: constraints,
1279 isDismissible: isDismissible,
1280 modalBarrierColor: barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
1281 enableDrag: enableDrag,
1282 showDragHandle: showDragHandle,
1283 settings: routeSettings,
1284 transitionAnimationController: transitionAnimationController,
1285 anchorPoint: anchorPoint,
1286 useSafeArea: useSafeArea,
1287 sheetAnimationStyle: sheetAnimationStyle,
1288 requestFocus: requestFocus,
1289 ),
1290 );
1291}
1292
1293/// Shows a Material Design bottom sheet in the nearest [Scaffold] ancestor. To
1294/// show a persistent bottom sheet, use the [Scaffold.bottomSheet].
1295///
1296/// Returns a controller that can be used to close and otherwise manipulate the
1297/// bottom sheet.
1298///
1299/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
1300/// [constraints] and [transitionAnimationController]
1301/// parameters can be passed in to customize the appearance and behavior of
1302/// persistent bottom sheets (see the documentation for these on [BottomSheet]
1303/// for more details).
1304///
1305/// The [enableDrag] parameter specifies whether the bottom sheet can be
1306/// dragged up and down and dismissed by swiping downwards.
1307///
1308/// The [sheetAnimationStyle] parameter is used to override the bottom sheet
1309/// animation duration and reverse animation duration.
1310///
1311/// If [AnimationStyle.duration] is provided, it will be used to override
1312/// the bottom sheet animation duration in the underlying
1313/// [BottomSheet.createAnimationController].
1314///
1315/// If [AnimationStyle.reverseDuration] is provided, it will be used to
1316/// override the bottom sheet reverse animation duration in the underlying
1317/// [BottomSheet.createAnimationController].
1318///
1319/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
1320///
1321/// {@tool dartpad}
1322/// This sample showcases how to override the [showBottomSheet] animation
1323/// duration and reverse animation duration using [AnimationStyle].
1324///
1325/// ** See code in examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart **
1326/// {@end-tool}
1327///
1328/// To rebuild the bottom sheet (e.g. if it is stateful), call
1329/// [PersistentBottomSheetController.setState] on the controller returned by
1330/// this method.
1331///
1332/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
1333/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
1334/// that closes the bottom sheet.
1335///
1336/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
1337/// does not add a back button to the enclosing Scaffold's app bar, use the
1338/// [Scaffold.bottomSheet] constructor parameter.
1339///
1340/// A closely related widget is a modal bottom sheet, which is an alternative
1341/// to a menu or a dialog and prevents the user from interacting with the rest
1342/// of the app. Modal bottom sheets can be created and displayed with the
1343/// [showModalBottomSheet] function.
1344///
1345/// The `context` argument is used to look up the [Scaffold] for the bottom
1346/// sheet. It is only used when the method is called. Its corresponding widget
1347/// can be safely removed from the tree before the bottom sheet is closed.
1348///
1349/// See also:
1350///
1351/// * [BottomSheet], which becomes the parent of the widget returned by the
1352/// `builder`.
1353/// * [showModalBottomSheet], which can be used to display a modal bottom
1354/// sheet.
1355/// * [Scaffold.of], for information about how to obtain the [BuildContext].
1356/// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
1357/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
1358/// * [AnimationStyle], which is used to override the bottom sheet animation
1359/// duration and reverse animation duration.
1360PersistentBottomSheetController showBottomSheet({
1361 required BuildContext context,
1362 required WidgetBuilder builder,
1363 Color? backgroundColor,
1364 double? elevation,
1365 ShapeBorder? shape,
1366 Clip? clipBehavior,
1367 BoxConstraints? constraints,
1368 bool? enableDrag,
1369 bool? showDragHandle,
1370 AnimationController? transitionAnimationController,
1371 AnimationStyle? sheetAnimationStyle,
1372}) {
1373 assert(debugCheckHasScaffold(context));
1374
1375 return Scaffold.of(context).showBottomSheet(
1376 builder,
1377 backgroundColor: backgroundColor,
1378 elevation: elevation,
1379 shape: shape,
1380 clipBehavior: clipBehavior,
1381 constraints: constraints,
1382 enableDrag: enableDrag,
1383 showDragHandle: showDragHandle,
1384 transitionAnimationController: transitionAnimationController,
1385 sheetAnimationStyle: sheetAnimationStyle,
1386 );
1387}
1388
1389class _BottomSheetGestureDetector extends StatelessWidget {
1390 const _BottomSheetGestureDetector({
1391 required this.child,
1392 required this.onVerticalDragStart,
1393 required this.onVerticalDragUpdate,
1394 required this.onVerticalDragEnd,
1395 });
1396
1397 final Widget child;
1398 final GestureDragStartCallback onVerticalDragStart;
1399 final GestureDragUpdateCallback onVerticalDragUpdate;
1400 final GestureDragEndCallback onVerticalDragEnd;
1401
1402 @override
1403 Widget build(BuildContext context) {
1404 return RawGestureDetector(
1405 excludeFromSemantics: true,
1406 gestures: <Type, GestureRecognizerFactory<GestureRecognizer>>{
1407 VerticalDragGestureRecognizer:
1408 GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
1409 () => VerticalDragGestureRecognizer(debugOwner: this),
1410 (VerticalDragGestureRecognizer instance) {
1411 instance
1412 ..onStart = onVerticalDragStart
1413 ..onUpdate = onVerticalDragUpdate
1414 ..onEnd = onVerticalDragEnd
1415 ..onlyAcceptDragOnThreshold = true;
1416 },
1417 ),
1418 },
1419 child: child,
1420 );
1421 }
1422}
1423
1424// BEGIN GENERATED TOKEN PROPERTIES - BottomSheet
1425
1426// Do not edit by hand. The code between the "BEGIN GENERATED" and
1427// "END GENERATED" comments are generated from data in the Material
1428// Design token database by the script:
1429// dev/tools/gen_defaults/bin/gen_defaults.dart.
1430
1431// dart format off
1432class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
1433 _BottomSheetDefaultsM3(this.context)
1434 : super(
1435 elevation: 1.0,
1436 modalElevation: 1.0,
1437 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))),
1438 constraints: const BoxConstraints(maxWidth: 640),
1439 );
1440
1441 final BuildContext context;
1442 late final ColorScheme _colors = Theme.of(context).colorScheme;
1443
1444 @override
1445 Color? get backgroundColor => _colors.surfaceContainerLow;
1446
1447 @override
1448 Color? get surfaceTintColor => Colors.transparent;
1449
1450 @override
1451 Color? get shadowColor => Colors.transparent;
1452
1453 @override
1454 Color? get dragHandleColor => _colors.onSurfaceVariant;
1455
1456 @override
1457 Size? get dragHandleSize => const Size(32, 4);
1458
1459 @override
1460 BoxConstraints? get constraints => const BoxConstraints(maxWidth: 640.0);
1461}
1462// dart format on
1463
1464// END GENERATED TOKEN PROPERTIES - BottomSheet
1465