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