Last active
May 14, 2026 08:14
-
-
Save tmaihoff/57f8c21c2cd08f482e4da63e5b975d5f to your computer and use it in GitHub Desktop.
Fix: Can't swipe to dismiss scrollable Bottom Sheet #36283
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250); | |
| const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); | |
| const Curve _modalBottomSheetCurve = Easing.legacyDecelerate; | |
| const double _minFlingVelocity = 700.0; | |
| const double _closeProgressThreshold = 0.5; | |
| const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0; | |
| /// A callback for when the user begins dragging the bottom sheet. | |
| /// | |
| /// Used by [BottomSheet.onDragStart]. | |
| typedef BottomSheetDragStartHandler = void Function(DragStartDetails details); | |
| /// A callback for when the user stops dragging the bottom sheet. | |
| /// | |
| /// Used by [BottomSheet.onDragEnd]. | |
| typedef BottomSheetDragEndHandler = void Function( | |
| DragEndDetails details, { | |
| required bool isClosing, | |
| }); | |
| /// A Material Design bottom sheet. | |
| /// | |
| /// There are two kinds of bottom sheets in Material Design: | |
| /// | |
| /// * _Persistent_. A persistent bottom sheet shows information that | |
| /// supplements the primary content of the app. A persistent bottom sheet | |
| /// remains visible even when the user interacts with other parts of the app. | |
| /// Persistent bottom sheets can be created and displayed with the | |
| /// [ScaffoldState.showBottomSheet] function or by specifying the | |
| /// [Scaffold.bottomSheet] constructor parameter. | |
| /// | |
| /// * _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 | |
| /// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or | |
| /// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet]. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing | |
| /// non-modal "persistent" bottom sheets. | |
| /// * [showModalBottomSheet], which can be used to display a modal bottom | |
| /// sheet. | |
| /// * [BottomSheetThemeData], which can be used to customize the default | |
| /// bottom sheet property values. | |
| /// * 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>. | |
| class BottomSheet extends StatefulWidget { | |
| /// Creates a bottom sheet. | |
| /// | |
| /// Typically, bottom sheets are created implicitly by | |
| /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by | |
| /// [showModalBottomSheet], for modal bottom sheets. | |
| const BottomSheet({ | |
| super.key, | |
| this.animationController, | |
| this.enableDrag = true, | |
| this.showDragHandle, | |
| this.dragHandleColor, | |
| this.dragHandleSize, | |
| this.onDragStart, | |
| this.onDragEnd, | |
| this.backgroundColor, | |
| this.shadowColor, | |
| this.elevation, | |
| this.shape, | |
| this.clipBehavior, | |
| this.constraints, | |
| required this.onClosing, | |
| required this.builder, | |
| }) : assert(elevation == null || elevation >= 0.0); | |
| /// 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. | |
| final AnimationController? animationController; | |
| /// Called when the bottom sheet begins to close. | |
| /// | |
| /// A bottom sheet might be prevented from closing (e.g., by user | |
| /// interaction) even after this callback is called. For this reason, this | |
| /// callback might be call multiple times for a given bottom sheet. | |
| final VoidCallback onClosing; | |
| /// 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; | |
| /// If true, the bottom sheet can be dragged up and down and dismissed by | |
| /// swiping downwards. | |
| /// | |
| /// If [showDragHandle] is true, this only applies to the content below the drag handle, | |
| /// because the drag handle is always draggable. | |
| /// | |
| /// Default is true. | |
| /// | |
| /// If this is true, the [animationController] must not be null. | |
| /// Use [BottomSheet.createAnimationController] to create one, or provide | |
| /// another AnimationController. | |
| final bool enableDrag; | |
| /// 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. | |
| /// | |
| /// If this is true, the [animationController] must not be null. | |
| /// Use [BottomSheet.createAnimationController] to create one, or provide | |
| /// another AnimationController. | |
| final bool? showDragHandle; | |
| /// The bottom sheet drag handle's color. | |
| /// | |
| /// Defaults to [BottomSheetThemeData.dragHandleColor]. | |
| /// If that is also null, defaults to [ColorScheme.onSurfaceVariant]. | |
| final Color? dragHandleColor; | |
| /// Defaults to [BottomSheetThemeData.dragHandleSize]. | |
| /// If that is also null, defaults to Size(32, 4). | |
| final Size? dragHandleSize; | |
| /// Called when the user begins dragging the bottom sheet vertically, if | |
| /// [enableDrag] is true. | |
| /// | |
| /// Would typically be used to change the bottom sheet animation curve so | |
| /// that it tracks the user's finger accurately. | |
| final BottomSheetDragStartHandler? onDragStart; | |
| /// Called when the user stops dragging the bottom sheet, if [enableDrag] | |
| /// is true. | |
| /// | |
| /// Would typically be used to reset the bottom sheet animation curve, so | |
| /// that it animates non-linearly. Called before [onClosing] if the bottom | |
| /// sheet is closing. | |
| final BottomSheetDragEndHandler? onDragEnd; | |
| /// The bottom sheet's background color. | |
| /// | |
| /// Defines the bottom sheet's [Material.color]. | |
| /// | |
| /// Defaults to null and falls back to [Material]'s default. | |
| final Color? backgroundColor; | |
| /// 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; | |
| /// 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. The value is non-negative. | |
| final double? elevation; | |
| /// The shape of the bottom sheet. | |
| /// | |
| /// Defines the bottom sheet's [Material.shape]. | |
| /// | |
| /// Defaults to null and falls back to [Material]'s default. | |
| final ShapeBorder? shape; | |
| /// {@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 then [BottomSheetThemeData.clipBehavior] of | |
| /// [ThemeData.bottomSheetTheme] is used. If that's null then the behavior | |
| /// will be [Clip.none]. | |
| final Clip? clipBehavior; | |
| /// Defines minimum and maximum sizes for a [BottomSheet]. | |
| /// | |
| /// If null, then the ambient [ThemeData.bottomSheetTheme]'s | |
| /// [BottomSheetThemeData.constraints] will be used. If that | |
| /// 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. | |
| /// | |
| /// 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; | |
| @override | |
| State<BottomSheet> createState() => _BottomSheetState(); | |
| /// Creates an [AnimationController] suitable for a | |
| /// [BottomSheet.animationController]. | |
| /// | |
| /// This API is available as a convenience for a Material compliant bottom sheet | |
| /// animation. If alternative animation durations are required, a different | |
| /// animation controller could be provided. | |
| static AnimationController createAnimationController( | |
| TickerProvider vsync, { | |
| AnimationStyle? sheetAnimationStyle, | |
| }) { | |
| return AnimationController( | |
| duration: sheetAnimationStyle?.duration ?? _bottomSheetEnterDuration, | |
| reverseDuration: | |
| sheetAnimationStyle?.reverseDuration ?? _bottomSheetExitDuration, | |
| debugLabel: 'BottomSheet', | |
| vsync: vsync, | |
| ); | |
| } | |
| } | |
| class _BottomSheetState extends State<BottomSheet> { | |
| final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child'); | |
| double get _childHeight { | |
| final RenderBox renderBox = | |
| _childKey.currentContext!.findRenderObject()! as RenderBox; | |
| return renderBox.size.height; | |
| } | |
| bool get _dismissUnderway => | |
| widget.animationController!.status == AnimationStatus.reverse; | |
| Set<WidgetState> dragHandleMaterialState = <WidgetState>{}; | |
| bool _isHandlingOverscroll = false; // Added state variable | |
| void _handleDragStart(DragStartDetails details) { | |
| setState(() { | |
| dragHandleMaterialState.add(WidgetState.dragged); | |
| }); | |
| widget.onDragStart?.call(details); | |
| } | |
| void _handleDragUpdate(DragUpdateDetails details) { | |
| assert( | |
| (widget.enableDrag || (widget.showDragHandle ?? false)) && | |
| widget.animationController != null, | |
| "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. " | |
| "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", | |
| ); | |
| if (_dismissUnderway) { | |
| return; | |
| } | |
| widget.animationController!.value -= details.primaryDelta! / _childHeight; | |
| } | |
| void _handleDragEnd(DragEndDetails details) { | |
| assert( | |
| (widget.enableDrag || (widget.showDragHandle ?? false)) && | |
| widget.animationController != null, | |
| "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. " | |
| "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", | |
| ); | |
| if (_dismissUnderway) { | |
| return; | |
| } | |
| setState(() { | |
| dragHandleMaterialState.remove(WidgetState.dragged); | |
| }); | |
| bool isClosing = false; | |
| if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) { | |
| final double flingVelocity = | |
| -details.velocity.pixelsPerSecond.dy / _childHeight; | |
| if (widget.animationController!.value > 0.0) { | |
| widget.animationController!.fling(velocity: flingVelocity); | |
| } | |
| if (flingVelocity < 0.0) { | |
| isClosing = true; | |
| } | |
| } else if (widget.animationController!.value < _closeProgressThreshold) { | |
| if (widget.animationController!.value > 0.0) { | |
| widget.animationController!.fling(velocity: -1.0); | |
| } | |
| isClosing = true; | |
| } else { | |
| widget.animationController!.forward(); | |
| } | |
| widget.onDragEnd?.call( | |
| details, | |
| isClosing: isClosing, | |
| ); | |
| if (isClosing) { | |
| widget.onClosing(); | |
| } | |
| } | |
| bool extentChanged(DraggableScrollableNotification notification) { | |
| if (notification.extent == notification.minExtent && | |
| notification.shouldCloseOnMinExtent) { | |
| widget.onClosing(); | |
| } | |
| return false; | |
| } | |
| // New method to handle scroll notifications from the child content | |
| bool _handleScrollNotification(ScrollNotification notification) { | |
| if (!widget.enableDrag || | |
| _dismissUnderway || // Check _dismissUnderway early | |
| widget.animationController == null) { | |
| return false; | |
| } | |
| if (notification is OverscrollNotification && | |
| notification.dragDetails != null && | |
| notification.metrics.extentBefore == 0.0 && | |
| notification.overscroll < 0) { | |
| // Dragging downwards at the top | |
| // We are now handling an overscroll that should drag the sheet. | |
| _isHandlingOverscroll = true; | |
| final double? delta = notification.dragDetails!.primaryDelta; | |
| // Ensure delta is positive (dragging down) and _childHeight is valid | |
| if (delta != null && delta > 0 && _childHeight > 0) { | |
| widget.animationController!.value -= delta / _childHeight; | |
| } | |
| // It's important to consume the notification to prevent the default overscroll effect | |
| // and to ensure our ScrollEndNotification logic below gets a chance. | |
| return true; | |
| } | |
| if (notification is ScrollEndNotification && _isHandlingOverscroll) { | |
| _isHandlingOverscroll = false; // Reset the flag | |
| // Use the dragDetails from the notification if available, otherwise, create neutral DragEndDetails. | |
| // This allows _handleDragEnd to correctly assess fling velocity or rely on position. | |
| final DragEndDetails endDetails = | |
| notification.dragDetails ?? DragEndDetails(primaryVelocity: 0.0); | |
| _handleDragEnd(endDetails); | |
| return true; // Consume the notification. | |
| } | |
| return false; // Don't consume other notifications. | |
| } | |
| void _handleDragHandleHover(bool hovering) { | |
| if (hovering != dragHandleMaterialState.contains(WidgetState.hovered)) { | |
| setState(() { | |
| if (hovering) { | |
| dragHandleMaterialState.add(WidgetState.hovered); | |
| } else { | |
| dragHandleMaterialState.remove(WidgetState.hovered); | |
| } | |
| }); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final BottomSheetThemeData bottomSheetTheme = | |
| Theme.of(context).bottomSheetTheme; | |
| final bool useMaterial3 = Theme.of(context).useMaterial3; | |
| final BottomSheetThemeData defaults = useMaterial3 | |
| ? _BottomSheetDefaultsM3(context) | |
| : const BottomSheetThemeData(); | |
| final BoxConstraints? constraints = widget.constraints ?? | |
| bottomSheetTheme.constraints ?? | |
| defaults.constraints; | |
| final Color? color = widget.backgroundColor ?? | |
| bottomSheetTheme.backgroundColor ?? | |
| defaults.backgroundColor; | |
| final Color? surfaceTintColor = | |
| bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor; | |
| final Color? shadowColor = widget.shadowColor ?? | |
| bottomSheetTheme.shadowColor ?? | |
| defaults.shadowColor; | |
| final double elevation = widget.elevation ?? | |
| bottomSheetTheme.elevation ?? | |
| defaults.elevation ?? | |
| 0; | |
| final ShapeBorder? shape = | |
| widget.shape ?? bottomSheetTheme.shape ?? defaults.shape; | |
| final Clip clipBehavior = | |
| widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none; | |
| 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, | |
| ); | |
| // Only add [_BottomSheetGestureDetector] to the drag handle when the rest of the | |
| // bottom sheet is not draggable. If the whole bottom sheet is draggable, | |
| // no need to add it. | |
| if (!widget.enableDrag) { | |
| dragHandle = _BottomSheetGestureDetector( | |
| onVerticalDragStart: _handleDragStart, | |
| onVerticalDragUpdate: _handleDragUpdate, | |
| onVerticalDragEnd: _handleDragEnd, | |
| child: dragHandle, | |
| ); | |
| } | |
| } | |
| // Wrap the user's content with the scroll notification listener | |
| final Widget userContent = widget.builder(context); | |
| final Widget wrappedUserContent = NotificationListener<ScrollNotification>( | |
| onNotification: _handleScrollNotification, | |
| child: userContent, | |
| ); | |
| Widget bottomSheetChild; | |
| if (!showDragHandle) { | |
| bottomSheetChild = wrappedUserContent; | |
| } else { | |
| // When showDragHandle is true, use a Column with MainAxisSize.min. | |
| // Wrap the wrappedUserContent with Flexible. This allows the Column to | |
| // shrink to the size of its children if the content is small. | |
| // If the content is large, Flexible allows wrappedUserContent to take | |
| // the remaining space within the constraints imposed on the Column by its parent, | |
| // enabling scrolling within wrappedUserContent and preventing overflow. | |
| bottomSheetChild = Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: <Widget>[ | |
| if (dragHandle != null) dragHandle, | |
| Flexible(child: wrappedUserContent), | |
| ], | |
| ); | |
| } | |
| Widget bottomSheet = Material( | |
| key: _childKey, | |
| color: color, | |
| elevation: elevation, | |
| surfaceTintColor: surfaceTintColor, | |
| shadowColor: shadowColor, | |
| shape: shape, | |
| clipBehavior: clipBehavior, | |
| child: NotificationListener<DraggableScrollableNotification>( | |
| onNotification: extentChanged, | |
| child: bottomSheetChild, // Use the (potentially) wrapped child | |
| ), | |
| ); | |
| if (constraints != null) { | |
| bottomSheet = Align( | |
| alignment: Alignment.bottomCenter, | |
| heightFactor: 1.0, | |
| child: ConstrainedBox( | |
| constraints: constraints, | |
| child: bottomSheet, | |
| ), | |
| ); | |
| } | |
| return !widget.enableDrag | |
| ? bottomSheet | |
| : _BottomSheetGestureDetector( | |
| onVerticalDragStart: _handleDragStart, | |
| onVerticalDragUpdate: _handleDragUpdate, | |
| onVerticalDragEnd: _handleDragEnd, | |
| child: bottomSheet, | |
| ); | |
| } | |
| } | |
| // PERSISTENT BOTTOM SHEETS | |
| // See scaffold.dart | |
| typedef _SizeChangeCallback<Size> = void Function(Size size); | |
| class _DragHandle extends StatelessWidget { | |
| const _DragHandle({ | |
| required this.onSemanticsTap, | |
| required this.handleHover, | |
| required this.materialState, | |
| this.dragHandleColor, | |
| this.dragHandleSize, | |
| }); | |
| final VoidCallback? onSemanticsTap; | |
| final ValueChanged<bool> handleHover; | |
| final Set<WidgetState> 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: WidgetStateProperty.resolveAs<Color?>( | |
| dragHandleColor, materialState) ?? | |
| WidgetStateProperty.resolveAs<Color?>( | |
| bottomSheetTheme.dragHandleColor, materialState) ?? | |
| m3Defaults.dragHandleColor, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget { | |
| const _BottomSheetLayoutWithSizeListener({ | |
| required this.onChildSizeChanged, | |
| required this.animationValue, | |
| required this.isScrollControlled, | |
| required this.scrollControlDisabledMaxHeightRatio, | |
| super.child, | |
| }); | |
| final _SizeChangeCallback<Size> onChildSizeChanged; | |
| final double animationValue; | |
| final bool isScrollControlled; | |
| final double scrollControlDisabledMaxHeightRatio; | |
| @override | |
| _RenderBottomSheetLayoutWithSizeListener createRenderObject( | |
| BuildContext context) { | |
| return _RenderBottomSheetLayoutWithSizeListener( | |
| onChildSizeChanged: onChildSizeChanged, | |
| animationValue: animationValue, | |
| isScrollControlled: isScrollControlled, | |
| scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, | |
| ); | |
| } | |
| @override | |
| void updateRenderObject(BuildContext context, | |
| _RenderBottomSheetLayoutWithSizeListener renderObject) { | |
| renderObject.onChildSizeChanged = onChildSizeChanged; | |
| renderObject.animationValue = animationValue; | |
| renderObject.isScrollControlled = isScrollControlled; | |
| renderObject.scrollControlDisabledMaxHeightRatio = | |
| scrollControlDisabledMaxHeightRatio; | |
| } | |
| } | |
| class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { | |
| _RenderBottomSheetLayoutWithSizeListener({ | |
| RenderBox? child, | |
| required _SizeChangeCallback<Size> onChildSizeChanged, | |
| required double animationValue, | |
| required bool isScrollControlled, | |
| required double scrollControlDisabledMaxHeightRatio, | |
| }) : _onChildSizeChanged = onChildSizeChanged, | |
| _animationValue = animationValue, | |
| _isScrollControlled = isScrollControlled, | |
| _scrollControlDisabledMaxHeightRatio = | |
| scrollControlDisabledMaxHeightRatio, | |
| 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(); | |
| } | |
| double get scrollControlDisabledMaxHeightRatio => | |
| _scrollControlDisabledMaxHeightRatio; | |
| double _scrollControlDisabledMaxHeightRatio; | |
| set scrollControlDisabledMaxHeightRatio(double newValue) { | |
| if (_scrollControlDisabledMaxHeightRatio == newValue) { | |
| return; | |
| } | |
| _scrollControlDisabledMaxHeightRatio = newValue; | |
| markNeedsLayout(); | |
| } | |
| 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; | |
| } | |
| @override | |
| 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); | |
| } | |
| @override | |
| double? computeDryBaseline( | |
| covariant BoxConstraints constraints, TextBaseline baseline) { | |
| final RenderBox? child = this.child; | |
| if (child == null) { | |
| return null; | |
| } | |
| final BoxConstraints childConstraints = | |
| _getConstraintsForChild(constraints); | |
| final double? result = child.getDryBaseline(childConstraints, baseline); | |
| if (result == null) { | |
| return null; | |
| } | |
| final Size childSize = childConstraints.isTight | |
| ? childConstraints.smallest | |
| : child.getDryLayout(childConstraints); | |
| return result + _getPositionForChild(_getSize(constraints), childSize).dy; | |
| } | |
| BoxConstraints _getConstraintsForChild(BoxConstraints constraints) { | |
| return BoxConstraints( | |
| minWidth: constraints.maxWidth, | |
| maxWidth: constraints.maxWidth, | |
| maxHeight: isScrollControlled | |
| ? constraints.maxHeight | |
| : constraints.maxHeight * scrollControlDisabledMaxHeightRatio, | |
| ); | |
| } | |
| Offset _getPositionForChild(Size size, Size childSize) { | |
| return Offset(0.0, size.height - childSize.height * animationValue); | |
| } | |
| @override | |
| void performLayout() { | |
| size = _getSize(constraints); | |
| final RenderBox? child = this.child; | |
| if (child == null) { | |
| return; | |
| } | |
| final BoxConstraints childConstraints = | |
| _getConstraintsForChild(constraints); | |
| assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); | |
| child.layout(childConstraints, parentUsesSize: !childConstraints.isTight); | |
| final BoxParentData childParentData = child.parentData! as BoxParentData; | |
| final Size childSize = | |
| childConstraints.isTight ? childConstraints.smallest : child.size; | |
| childParentData.offset = _getPositionForChild(size, childSize); | |
| if (_lastSize != childSize) { | |
| _lastSize = childSize; | |
| _onChildSizeChanged.call(_lastSize); | |
| } | |
| } | |
| } | |
| class _ModalBottomSheet<T> extends StatefulWidget { | |
| const _ModalBottomSheet({ | |
| super.key, | |
| required this.route, | |
| this.backgroundColor, | |
| this.elevation, | |
| this.shape, | |
| this.clipBehavior, | |
| this.constraints, | |
| this.isScrollControlled = false, | |
| this.scrollControlDisabledMaxHeightRatio = | |
| _defaultScrollControlDisabledMaxHeightRatio, | |
| this.enableDrag = true, | |
| this.showDragHandle = false, | |
| }); | |
| final ModalBottomSheetRoute<T> route; | |
| final bool isScrollControlled; | |
| final double scrollControlDisabledMaxHeightRatio; | |
| final Color? backgroundColor; | |
| final double? elevation; | |
| final ShapeBorder? shape; | |
| final Clip? clipBehavior; | |
| final BoxConstraints? constraints; | |
| final bool enableDrag; | |
| final bool showDragHandle; | |
| @override | |
| _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>(); | |
| } | |
| class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> { | |
| ParametricCurve<double> animationCurve = _modalBottomSheetCurve; | |
| String _getRouteLabel(MaterialLocalizations localizations) { | |
| switch (Theme.of(context).platform) { | |
| case TargetPlatform.iOS: | |
| case TargetPlatform.macOS: | |
| return ''; | |
| case TargetPlatform.android: | |
| case TargetPlatform.fuchsia: | |
| case TargetPlatform.linux: | |
| case TargetPlatform.windows: | |
| return localizations.dialogLabel; | |
| } | |
| } | |
| EdgeInsets _getNewClipDetails(Size topLayerSize) { | |
| return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height); | |
| } | |
| void handleDragStart(DragStartDetails details) { | |
| // Allow the bottom sheet to track the user's finger accurately. | |
| animationCurve = Curves.linear; | |
| } | |
| void handleDragEnd(DragEndDetails details, {bool? isClosing}) { | |
| // Allow the bottom sheet to animate smoothly from its current position. | |
| animationCurve = Split( | |
| widget.route.animation!.value, | |
| endCurve: _modalBottomSheetCurve, | |
| ); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| assert(debugCheckHasMediaQuery(context)); | |
| assert(debugCheckHasMaterialLocalizations(context)); | |
| final MaterialLocalizations localizations = | |
| MaterialLocalizations.of(context); | |
| final String routeLabel = _getRouteLabel(localizations); | |
| return AnimatedBuilder( | |
| animation: widget.route.animation!, | |
| child: BottomSheet( | |
| animationController: widget.route._animationController, | |
| onClosing: () { | |
| if (widget.route.isCurrent) { | |
| Navigator.pop(context); | |
| } | |
| }, | |
| builder: widget.route.builder, | |
| backgroundColor: widget.backgroundColor, | |
| elevation: widget.elevation, | |
| shape: widget.shape, | |
| clipBehavior: widget.clipBehavior, | |
| constraints: widget.constraints, | |
| enableDrag: widget.enableDrag, | |
| showDragHandle: widget.showDragHandle, | |
| onDragStart: handleDragStart, | |
| onDragEnd: handleDragEnd, | |
| ), | |
| builder: (BuildContext context, Widget? child) { | |
| final double animationValue = animationCurve.transform( | |
| widget.route.animation!.value, | |
| ); | |
| return Semantics( | |
| scopesRoute: true, | |
| namesRoute: true, | |
| label: routeLabel, | |
| explicitChildNodes: true, | |
| child: ClipRect( | |
| child: _BottomSheetLayoutWithSizeListener( | |
| onChildSizeChanged: (Size size) { | |
| widget.route._didChangeBarrierSemanticsClip( | |
| _getNewClipDetails(size), | |
| ); | |
| }, | |
| animationValue: animationValue, | |
| isScrollControlled: widget.isScrollControlled, | |
| scrollControlDisabledMaxHeightRatio: | |
| widget.scrollControlDisabledMaxHeightRatio, | |
| child: child, | |
| ), | |
| ), | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| /// 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. | |
| /// | |
| /// The [useSafeArea] parameter specifies whether the sheet will avoid system | |
| /// 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. | |
| /// Defaults to false. | |
| /// | |
| /// 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. | |
| /// * 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>. | |
| class ModalBottomSheetRoute<T> extends PopupRoute<T> { | |
| /// A modal bottom sheet route. | |
| ModalBottomSheetRoute({ | |
| required this.builder, | |
| this.capturedThemes, | |
| this.barrierLabel, | |
| this.barrierOnTapHint, | |
| this.backgroundColor, | |
| this.elevation, | |
| this.shape, | |
| this.clipBehavior, | |
| this.constraints, | |
| this.modalBarrierColor, | |
| this.isDismissible = true, | |
| this.enableDrag = true, | |
| this.showDragHandle, | |
| required this.isScrollControlled, | |
| this.scrollControlDisabledMaxHeightRatio = | |
| _defaultScrollControlDisabledMaxHeightRatio, | |
| super.settings, | |
| this.transitionAnimationController, | |
| this.anchorPoint, | |
| this.useSafeArea = false, | |
| this.sheetAnimationStyle, | |
| }); | |
| /// 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. | |
| final bool isScrollControlled; | |
| /// The max height constraint ratio for the bottom sheet | |
| /// when [isScrollControlled] is set to false, | |
| /// no ratio will be applied when [isScrollControlled] is set to true. | |
| /// | |
| /// Defaults to 9 / 16. | |
| final double scrollControlDisabledMaxHeightRatio; | |
| /// 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. | |
| final Color? backgroundColor; | |
| /// 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. | |
| final double? elevation; | |
| /// 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. | |
| final ShapeBorder? shape; | |
| /// {@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]. | |
| final Clip? clipBehavior; | |
| /// Defines minimum and maximum sizes for a [BottomSheet]. | |
| /// | |
| /// If null, the ambient [ThemeData.bottomSheetTheme]'s | |
| /// [BottomSheetThemeData.constraints] will be used. If that | |
| /// 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. | |
| /// | |
| /// 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; | |
| /// Specifies the color of the modal barrier that darkens everything below the | |
| /// bottom sheet. | |
| /// | |
| /// Defaults to `Colors.black54` if not provided. | |
| final Color? modalBarrierColor; | |
| /// 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. | |
| final bool isDismissible; | |
| /// 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. | |
| /// | |
| /// This applies to the content below the drag handle, if showDragHandle is true. | |
| /// | |
| /// Defaults is true. | |
| final bool enableDrag; | |
| /// 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; | |
| /// 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. | |
| final AnimationController? transitionAnimationController; | |
| /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint} | |
| final Offset? anchorPoint; | |
| /// Whether to avoid system intrusions on the top, left, and right. | |
| /// | |
| /// 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. | |
| /// | |
| /// 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]. | |
| /// | |
| /// In either case, the bottom sheet extends all the way to the bottom of | |
| /// the screen, including any system intrusions. | |
| /// | |
| /// The default is false. | |
| final bool useSafeArea; | |
| /// Used to override the modal bottom sheet animation duration and reverse | |
| /// animation duration. | |
| /// | |
| /// If [AnimationStyle.duration] is provided, it will be used to override | |
| /// the modal bottom sheet animation duration in the underlying | |
| /// [BottomSheet.createAnimationController]. | |
| /// | |
| /// If [AnimationStyle.reverseDuration] is provided, it will be used to | |
| /// override the modal bottom sheet reverse animation duration in the | |
| /// underlying [BottomSheet.createAnimationController]. | |
| /// | |
| /// To disable the modal bottom sheet animation, use [AnimationStyle.noAnimation]. | |
| final AnimationStyle? sheetAnimationStyle; | |
| /// {@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); | |
| @override | |
| void dispose() { | |
| _clipDetailsNotifier.dispose(); | |
| super.dispose(); | |
| } | |
| /// 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; | |
| } | |
| @override | |
| Duration get transitionDuration => _bottomSheetEnterDuration; | |
| @override | |
| Duration get reverseTransitionDuration => _bottomSheetExitDuration; | |
| @override | |
| bool get barrierDismissible => isDismissible; | |
| @override | |
| final String? barrierLabel; | |
| @override | |
| Color get barrierColor => modalBarrierColor ?? Colors.black54; | |
| AnimationController? _animationController; | |
| @override | |
| AnimationController createAnimationController() { | |
| assert(_animationController == null); | |
| if (transitionAnimationController != null) { | |
| _animationController = transitionAnimationController; | |
| willDisposeAnimationController = false; | |
| } else { | |
| _animationController = BottomSheet.createAnimationController( | |
| navigator!, | |
| sheetAnimationStyle: sheetAnimationStyle, | |
| ); | |
| } | |
| return _animationController!; | |
| } | |
| @override | |
| Widget buildPage(BuildContext context, Animation<double> animation, | |
| Animation<double> secondaryAnimation) { | |
| final Widget content = DisplayFeatureSubScreen( | |
| anchorPoint: anchorPoint, | |
| child: Builder( | |
| builder: (BuildContext context) { | |
| final BottomSheetThemeData sheetTheme = | |
| Theme.of(context).bottomSheetTheme; | |
| final BottomSheetThemeData defaults = Theme.of(context).useMaterial3 | |
| ? _BottomSheetDefaultsM3(context) | |
| : const BottomSheetThemeData(); | |
| return _ModalBottomSheet<T>( | |
| route: this, | |
| backgroundColor: backgroundColor ?? | |
| sheetTheme.modalBackgroundColor ?? | |
| sheetTheme.backgroundColor ?? | |
| defaults.backgroundColor, | |
| elevation: elevation ?? | |
| sheetTheme.modalElevation ?? | |
| sheetTheme.elevation ?? | |
| defaults.modalElevation, | |
| shape: shape, | |
| clipBehavior: clipBehavior, | |
| constraints: constraints, | |
| isScrollControlled: isScrollControlled, | |
| scrollControlDisabledMaxHeightRatio: | |
| scrollControlDisabledMaxHeightRatio, | |
| enableDrag: enableDrag, | |
| showDragHandle: showDragHandle ?? | |
| (enableDrag && (sheetTheme.showDragHandle ?? false)), | |
| ); | |
| }, | |
| ), | |
| ); | |
| final Widget bottomSheet = useSafeArea | |
| ? SafeArea(bottom: false, child: content) | |
| : MediaQuery.removePadding( | |
| context: context, | |
| removeTop: true, | |
| child: content, | |
| ); | |
| return capturedThemes?.wrap(bottomSheet) ?? bottomSheet; | |
| } | |
| @override | |
| Widget buildModalBarrier() { | |
| if (barrierColor.alpha != 0 && !offstage) { | |
| // changedInternalState is called if barrierColor or offstage updates | |
| 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, | |
| ); | |
| } | |
| } | |
| } | |
| /// Shows a modal Material Design bottom sheet. | |
| /// | |
| /// {@macro flutter.material.ModalBottomSheetRoute} | |
| /// | |
| /// {@macro flutter.widgets.RawDialogRoute} | |
| /// | |
| /// 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. | |
| /// | |
| /// 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]. | |
| /// | |
| /// Returns a `Future` that resolves to the value (if any) that was passed to | |
| /// [Navigator.pop] when the modal bottom sheet was closed. | |
| /// | |
| /// The 'barrierLabel' parameter can be used to set a custom barrier label. | |
| /// Will default to [MaterialLocalizations.modalBarrierDismissLabel] of context | |
| /// if not set. | |
| /// | |
| /// {@tool dartpad} | |
| /// This example demonstrates how to use [showModalBottomSheet] to display a | |
| /// bottom sheet that obscures the content behind it when a user taps a button. | |
| /// It also demonstrates how to close the bottom sheet using the [Navigator] | |
| /// when a user taps on a button inside the bottom sheet. | |
| /// | |
| /// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.0.dart ** | |
| /// {@end-tool} | |
| /// | |
| /// {@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} | |
| /// | |
| /// The [sheetAnimationStyle] parameter is used to override the modal bottom sheet | |
| /// animation duration and reverse animation duration. | |
| /// | |
| /// If [AnimationStyle.duration] is provided, it will be used to override | |
| /// the modal bottom sheet animation duration in the underlying | |
| /// [BottomSheet.createAnimationController]. | |
| /// | |
| /// If [AnimationStyle.reverseDuration] is provided, it will be used to | |
| /// override the modal bottom sheet reverse animation duration in the | |
| /// underlying [BottomSheet.createAnimationController]. | |
| /// | |
| /// To disable the bottom sheet animation, use [AnimationStyle.noAnimation]. | |
| /// | |
| /// {@tool dartpad} | |
| /// This sample showcases how to override the [showModalBottomSheet] animation | |
| /// duration and reverse animation duration using [AnimationStyle]. | |
| /// | |
| /// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart ** | |
| /// {@end-tool} | |
| /// | |
| /// See also: | |
| /// | |
| /// * [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. | |
| /// * 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>. | |
| /// * [AnimationStyle], which is used to override the modal bottom sheet | |
| /// animation duration and reverse animation duration. | |
| Future<T?> showModalBottomSheetFix<T>({ | |
| required BuildContext context, | |
| required WidgetBuilder builder, | |
| Color? backgroundColor, | |
| String? barrierLabel, | |
| double? elevation, | |
| ShapeBorder? shape, | |
| Clip? clipBehavior, | |
| BoxConstraints? constraints, | |
| Color? barrierColor, | |
| bool isScrollControlled = false, | |
| double scrollControlDisabledMaxHeightRatio = | |
| _defaultScrollControlDisabledMaxHeightRatio, | |
| bool useRootNavigator = false, | |
| bool isDismissible = true, | |
| bool enableDrag = true, | |
| bool? showDragHandle, | |
| bool useSafeArea = false, | |
| RouteSettings? routeSettings, | |
| AnimationController? transitionAnimationController, | |
| Offset? anchorPoint, | |
| AnimationStyle? sheetAnimationStyle, | |
| }) { | |
| assert(debugCheckHasMediaQuery(context)); | |
| assert(debugCheckHasMaterialLocalizations(context)); | |
| final NavigatorState navigator = | |
| Navigator.of(context, rootNavigator: useRootNavigator); | |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context); | |
| return navigator.push(ModalBottomSheetRoute<T>( | |
| builder: builder, | |
| capturedThemes: | |
| InheritedTheme.capture(from: context, to: navigator.context), | |
| isScrollControlled: isScrollControlled, | |
| scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, | |
| barrierLabel: barrierLabel ?? localizations.scrimLabel, | |
| barrierOnTapHint: | |
| localizations.scrimOnTapHint(localizations.bottomSheetLabel), | |
| backgroundColor: backgroundColor, | |
| elevation: elevation, | |
| shape: shape, | |
| clipBehavior: clipBehavior, | |
| constraints: constraints, | |
| isDismissible: isDismissible, | |
| modalBarrierColor: | |
| barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor, | |
| enableDrag: enableDrag, | |
| showDragHandle: showDragHandle, | |
| settings: routeSettings, | |
| transitionAnimationController: transitionAnimationController, | |
| anchorPoint: anchorPoint, | |
| useSafeArea: useSafeArea, | |
| sheetAnimationStyle: sheetAnimationStyle, | |
| )); | |
| } | |
| /// Shows a Material Design bottom sheet in the nearest [Scaffold] ancestor. To | |
| /// show a persistent bottom sheet, use the [Scaffold.bottomSheet]. | |
| /// | |
| /// Returns a controller that can be used to close and otherwise manipulate the | |
| /// bottom sheet. | |
| /// | |
| /// The optional [backgroundColor], [elevation], [shape], [clipBehavior], | |
| /// [constraints] and [transitionAnimationController] | |
| /// parameters can be passed in to customize the appearance and behavior of | |
| /// persistent bottom sheets (see the documentation for these on [BottomSheet] | |
| /// for more details). | |
| /// | |
| /// The [enableDrag] parameter specifies whether the bottom sheet can be | |
| /// dragged up and down and dismissed by swiping downwards. | |
| /// | |
| /// The [sheetAnimationStyle] parameter is used to override the bottom sheet | |
| /// animation duration and reverse animation duration. | |
| /// | |
| /// If [AnimationStyle.duration] is provided, it will be used to override | |
| /// the bottom sheet animation duration in the underlying | |
| /// [BottomSheet.createAnimationController]. | |
| /// | |
| /// If [AnimationStyle.reverseDuration] is provided, it will be used to | |
| /// override the bottom sheet reverse animation duration in the underlying | |
| /// [BottomSheet.createAnimationController]. | |
| /// | |
| /// To disable the bottom sheet animation, use [AnimationStyle.noAnimation]. | |
| /// | |
| /// {@tool dartpad} | |
| /// This sample showcases how to override the [showBottomSheet] animation | |
| /// duration and reverse animation duration using [AnimationStyle]. | |
| /// | |
| /// ** See code in examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart ** | |
| /// {@end-tool} | |
| /// | |
| /// 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 | |
| /// [ModalRoute] and a back button is added to the app bar of the [Scaffold] | |
| /// that closes the bottom sheet. | |
| /// | |
| /// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and | |
| /// does not add a back button to the enclosing Scaffold's app bar, use the | |
| /// [Scaffold.bottomSheet] constructor parameter. | |
| /// | |
| /// 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: | |
| /// | |
| /// * [BottomSheet], which becomes the parent of the widget returned by the | |
| /// `builder`. | |
| /// * [showModalBottomSheet], which can be used to display a modal bottom | |
| /// sheet. | |
| /// * [Scaffold.of], for information about how to obtain the [BuildContext]. | |
| /// * 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>. | |
| /// * [AnimationStyle], which is used to override the bottom sheet animation | |
| /// duration and reverse animation duration. | |
| PersistentBottomSheetController showBottomSheet({ | |
| required BuildContext context, | |
| required WidgetBuilder builder, | |
| Color? backgroundColor, | |
| double? elevation, | |
| ShapeBorder? shape, | |
| Clip? clipBehavior, | |
| BoxConstraints? constraints, | |
| bool? enableDrag, | |
| bool? showDragHandle, | |
| AnimationController? transitionAnimationController, | |
| AnimationStyle? sheetAnimationStyle, | |
| }) { | |
| assert(debugCheckHasScaffold(context)); | |
| return Scaffold.of(context).showBottomSheet( | |
| builder, | |
| backgroundColor: backgroundColor, | |
| elevation: elevation, | |
| shape: shape, | |
| clipBehavior: clipBehavior, | |
| constraints: constraints, | |
| enableDrag: enableDrag, | |
| showDragHandle: showDragHandle, | |
| transitionAnimationController: transitionAnimationController, | |
| sheetAnimationStyle: sheetAnimationStyle, | |
| ); | |
| } | |
| 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; | |
| @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, | |
| ); | |
| } | |
| } | |
| // 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 { | |
| _BottomSheetDefaultsM3(this.context) | |
| : super( | |
| elevation: 1.0, | |
| modalElevation: 1.0, | |
| shape: const RoundedRectangleBorder( | |
| borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))), | |
| constraints: const BoxConstraints(maxWidth: 640), | |
| ); | |
| final BuildContext context; | |
| late final ColorScheme _colors = Theme.of(context).colorScheme; | |
| @override | |
| Color? get backgroundColor => _colors.surfaceContainerLow; | |
| @override | |
| Color? get surfaceTintColor => Colors.transparent; | |
| @override | |
| Color? get shadowColor => Colors.transparent; | |
| @override | |
| Color? get dragHandleColor => _colors.onSurfaceVariant; | |
| @override | |
| Size? get dragHandleSize => const Size(32, 4); | |
| @override | |
| BoxConstraints? get constraints => const BoxConstraints(maxWidth: 640.0); | |
| } | |
| // END GENERATED TOKEN PROPERTIES - BottomSheet |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment