· 6 years ago · Jan 31, 2020, 12:04 AM
1// Copyright 2015 The Chromium 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
5import 'dart:async';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/scheduler.dart';
9import 'package:flutter/widgets.dart';
10
11import 'bottom_sheet_theme.dart';
12import 'colors.dart';
13import 'debug.dart';
14import 'material.dart';
15import 'material_localizations.dart';
16import 'scaffold.dart';
17import 'theme.dart';
18
19const Duration _bottomSheetDuration = Duration(milliseconds: 450);
20const double _minFlingVelocity = 1.0;
21const double _closeProgressThreshold = 0.2;
22
23/// A material design bottom sheet.
24///
25/// There are two kinds of bottom sheets in material design:
26///
27/// * _Persistent_. A persistent bottom sheet shows information that
28/// supplements the primary content of the app. A persistent bottom sheet
29/// remains visible even when the user interacts with other parts of the app.
30/// Persistent bottom sheets can be created and displayed with the
31/// [ScaffoldState.showBottomSheet] function or by specifying the
32/// [Scaffold.bottomSheet] constructor parameter.
33///
34/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and
35/// prevents the user from interacting with the rest of the app. Modal bottom
36/// sheets can be created and displayed with the [showModalBottomSheet]
37/// function.
38///
39/// The [BottomSheet] widget itself is rarely used directly. Instead, prefer to
40/// create a persistent bottom sheet with [ScaffoldState.showBottomSheet] or
41/// [Scaffold.bottomSheet], and a modal bottom sheet with [showModalBottomSheet].
42///
43/// See also:
44///
45/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
46/// non-modal "persistent" bottom sheets.
47/// * [showModalBottomSheet], which can be used to display a modal bottom
48/// sheet.
49/// * <https://material.io/design/components/sheets-bottom.html>
50class BottomSheet extends StatefulWidget {
51 /// Creates a bottom sheet.
52 ///
53 /// Typically, bottom sheets are created implicitly by
54 /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by
55 /// [showModalBottomSheet], for modal bottom sheets.
56 const BottomSheet({
57 Key key,
58 this.animationController,
59 this.enableDrag = true,
60 this.backgroundColor,
61 this.elevation,
62 this.shape,
63 this.clipBehavior,
64 @required this.onClosing,
65 @required this.builder,
66 }) : assert(enableDrag != null),
67 assert(onClosing != null),
68 assert(builder != null),
69 assert(elevation == null || elevation >= 0.0),
70 super(key: key);
71
72 /// The animation controller that controls the bottom sheet's entrance and
73 /// exit animations.
74 ///
75 /// The BottomSheet widget will manipulate the position of this animation, it
76 /// is not just a passive observer.
77 final AnimationController animationController;
78
79 /// Called when the bottom sheet begins to close.
80 ///
81 /// A bottom sheet might be prevented from closing (e.g., by user
82 /// interaction) even after this callback is called. For this reason, this
83 /// callback might be call multiple times for a given bottom sheet.
84 final VoidCallback onClosing;
85
86 /// A builder for the contents of the sheet.
87 ///
88 /// The bottom sheet will wrap the widget produced by this builder in a
89 /// [Material] widget.
90 final WidgetBuilder builder;
91
92 /// If true, the bottom sheet can be dragged up and down and dismissed by
93 /// swiping downwards.
94 ///
95 /// Default is true.
96 final bool enableDrag;
97
98 /// The bottom sheet's background color.
99 ///
100 /// Defines the bottom sheet's [Material.color].
101 ///
102 /// Defaults to null and falls back to [Material]'s default.
103 final Color backgroundColor;
104
105 /// The z-coordinate at which to place this material relative to its parent.
106 ///
107 /// This controls the size of the shadow below the material.
108 ///
109 /// Defaults to 0. The value is non-negative.
110 final double elevation;
111
112 /// The shape of the bottom sheet.
113 ///
114 /// Defines the bottom sheet's [Material.shape].
115 ///
116 /// Defaults to null and falls back to [Material]'s default.
117 final ShapeBorder shape;
118
119 /// {@macro flutter.widgets.Clip}
120 ///
121 /// Defines the bottom sheet's [Material.clipBehavior].
122 ///
123 /// Use this property to enable clipping of content when the bottom sheet has
124 /// a custom [shape] and the content can extend past this shape. For example,
125 /// a bottom sheet with rounded corners and an edge-to-edge [Image] at the
126 /// top.
127 ///
128 /// If this property is null then [ThemeData.bottomSheetTheme.clipBehavior] is
129 /// used. If that's null then the behavior will be [Clip.none].
130 final Clip clipBehavior;
131
132 @override
133 _BottomSheetState createState() => _BottomSheetState();
134
135 /// Creates an [AnimationController] suitable for a
136 /// [BottomSheet.animationController].
137 ///
138 /// This API available as a convenience for a Material compliant bottom sheet
139 /// animation. If alternative animation durations are required, a different
140 /// animation controller could be provided.
141 static AnimationController createAnimationController(TickerProvider vsync) {
142 return AnimationController(
143 duration: _bottomSheetDuration,
144 debugLabel: 'BottomSheet',
145 vsync: vsync,
146 );
147 }
148}
149
150class _BottomSheetState extends State<BottomSheet> {
151
152 final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child');
153
154 double get _childHeight {
155 final RenderBox renderBox = _childKey.currentContext.findRenderObject();
156 return renderBox.size.height;
157 }
158
159 bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
160
161 void _handleDragUpdate(DragUpdateDetails details) {
162 assert(widget.enableDrag);
163 if (_dismissUnderway)
164 return;
165 widget.animationController.value -= details.primaryDelta / (_childHeight ?? details.primaryDelta);
166 }
167
168 void _handleDragEnd(DragEndDetails details) {
169 assert(widget.enableDrag);
170 if (_dismissUnderway)
171 return;
172 if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
173 final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
174 if (widget.animationController.value > 0.0) {
175 widget.animationController.fling(velocity: flingVelocity);
176 }
177 if (flingVelocity < 0.0) {
178 widget.onClosing();
179 }
180 } else if (widget.animationController.value < _closeProgressThreshold) {
181 if (widget.animationController.value > 0.0)
182 widget.animationController.fling(velocity: -1.0);
183 widget.onClosing();
184 } else {
185 widget.animationController.forward();
186 }
187 }
188
189 bool extentChanged(DraggableScrollableNotification notification) {
190 if (notification.extent == notification.minExtent) {
191 widget.onClosing();
192 }
193 return false;
194 }
195
196 @override
197 Widget build(BuildContext context) {
198 final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
199 final Color color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
200 final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? 0;
201 final ShapeBorder shape = widget.shape ?? bottomSheetTheme.shape;
202 final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
203
204 final Widget bottomSheet = Material(
205 key: _childKey,
206 color: color,
207 elevation: elevation,
208 shape: shape,
209 clipBehavior: clipBehavior,
210 child: NotificationListener<DraggableScrollableNotification>(
211 onNotification: extentChanged,
212 child: widget.builder(context),
213 ),
214 );
215 return !widget.enableDrag ? bottomSheet : GestureDetector(
216 onVerticalDragUpdate: _handleDragUpdate,
217 onVerticalDragEnd: _handleDragEnd,
218 child: bottomSheet,
219 excludeFromSemantics: true,
220 );
221 }
222}
223
224// PERSISTENT BOTTOM SHEETS
225
226// See scaffold.dart
227
228
229// MODAL BOTTOM SHEETS
230class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
231 _ModalBottomSheetLayout(this.progress, this.isScrollControlled);
232
233 final double progress;
234 final bool isScrollControlled;
235
236 @override
237 BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
238 return BoxConstraints(
239 minWidth: constraints.maxWidth,
240 maxWidth: constraints.maxWidth,
241 minHeight: 0.0,
242 maxHeight: isScrollControlled
243 ? constraints.maxHeight
244 : constraints.maxHeight * 9.0 / 16.0,
245 );
246 }
247
248 @override
249 Offset getPositionForChild(Size size, Size childSize) {
250 return Offset(0.0, size.height - childSize.height * progress);
251 }
252
253 @override
254 bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
255 return progress != oldDelegate.progress;
256 }
257}
258
259class _ModalBottomSheet<T> extends StatefulWidget {
260 const _ModalBottomSheet({
261 Key key,
262 this.route,
263 this.backgroundColor,
264 this.elevation,
265 this.shape,
266 this.clipBehavior,
267 this.isScrollControlled = false,
268 }) : assert(isScrollControlled != null),
269 super(key: key);
270
271 final _ModalBottomSheetRoute<T> route;
272 final bool isScrollControlled;
273 final Color backgroundColor;
274 final double elevation;
275 final ShapeBorder shape;
276 final Clip clipBehavior;
277
278 @override
279 _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
280}
281
282class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
283 String _getRouteLabel(MaterialLocalizations localizations) {
284 switch (Theme.of(context).platform) {
285 case TargetPlatform.iOS:
286 return '';
287 case TargetPlatform.android:
288 case TargetPlatform.fuchsia:
289 return localizations.dialogLabel;
290 }
291 return null;
292 }
293
294 @override
295 Widget build(BuildContext context) {
296 assert(debugCheckHasMediaQuery(context));
297 assert(debugCheckHasMaterialLocalizations(context));
298 final MediaQueryData mediaQuery = MediaQuery.of(context);
299 final MaterialLocalizations localizations = MaterialLocalizations.of(context);
300 final String routeLabel = _getRouteLabel(localizations);
301
302 return AnimatedBuilder(
303 animation: widget.route.animation,
304 builder: (BuildContext context, Widget child) {
305 // Disable the initial animation when accessible navigation is on so
306 // that the semantics are added to the tree at the correct time.
307 final double animationValue = mediaQuery.accessibleNavigation ? 1.0 : widget.route.animation.value;
308 return Semantics(
309 scopesRoute: true,
310 namesRoute: true,
311 label: routeLabel,
312 explicitChildNodes: true,
313 child: ClipRect(
314 child: CustomSingleChildLayout(
315 delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
316 child: BottomSheet(
317 animationController: widget.route._animationController,
318 onClosing: () {
319 if (widget.route.isCurrent) {
320 Navigator.pop(context);
321 }
322 },
323 builder: widget.route.builder,
324 backgroundColor: widget.backgroundColor,
325 elevation: widget.elevation,
326 shape: widget.shape,
327 clipBehavior: widget.clipBehavior,
328 ),
329 ),
330 ),
331 );
332 },
333 );
334 }
335}
336
337class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
338 _ModalBottomSheetRoute({
339 this.builder,
340 this.animate,
341 this.theme,
342 this.barrierLabel,
343 this.backgroundColor,
344 this.elevation,
345 this.shape,
346 this.clipBehavior,
347 this.isDismissible = true,
348 @required this.isScrollControlled,
349 RouteSettings settings,
350 }) : assert(isScrollControlled != null),
351 assert(isDismissible != null),
352 super(settings: settings);
353
354 final WidgetBuilder builder;
355 final bool animate;
356 final ThemeData theme;
357 final bool isScrollControlled;
358 final Color backgroundColor;
359 final double elevation;
360 final ShapeBorder shape;
361 final Clip clipBehavior;
362 final bool isDismissible;
363
364 @override
365 Duration get transitionDuration => _bottomSheetDuration;
366
367 @override
368 bool get barrierDismissible => isDismissible;
369
370 @override
371 final String barrierLabel;
372
373 @override
374 Color get barrierColor => Colors.black54;
375
376 Animation<double> _animation;
377 Tween<Offset> _offsetTween;
378
379 AnimationController _animationController;
380
381 @override
382 Animation<double> createAnimation() {
383 assert(_animation == null);
384 _animation = CurvedAnimation(
385 parent: super.createAnimation(),
386
387 // These curves were initially measured from native iOS horizontal page
388 // route animations and seemed to be a good match here as well.
389 curve: Curves.linearToEaseOut,
390 reverseCurve: Curves.linearToEaseOut.flipped,
391 );
392 _offsetTween = Tween<Offset>(
393 begin: const Offset(0.0, 1.0),
394 end: const Offset(0.0, 0.0),
395 );
396 return _animation;
397 }
398
399 @override
400 AnimationController createAnimationController() {
401 assert(_animationController == null);
402 _animationController = BottomSheet.createAnimationController(navigator.overlay);
403 return _animationController;
404 }
405
406 @override
407 Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
408 final BottomSheetThemeData sheetTheme = theme?.bottomSheetTheme ?? Theme.of(context).bottomSheetTheme;
409 // By definition, the bottom sheet is aligned to the bottom of the page
410 // and isn't exposed to the top padding of the MediaQuery.
411 Widget bottomSheet = MediaQuery.removePadding(
412 context: context,
413 removeTop: true,
414 child: _ModalBottomSheet<T>(
415 route: this,
416 backgroundColor: backgroundColor ?? sheetTheme?.modalBackgroundColor ?? sheetTheme?.backgroundColor,
417 elevation: elevation ?? sheetTheme?.modalElevation ?? sheetTheme?.elevation,
418 shape: shape,
419 clipBehavior: clipBehavior,
420 isScrollControlled: isScrollControlled,
421 ),
422 );
423 if (theme != null)
424 bottomSheet = Theme(data: theme, child: bottomSheet);
425
426 AnimationController _controller = AnimationController(vsync: navigator.overlay, duration: Duration(milliseconds: 380));
427 Animation _animation = CurvedAnimation(parent: _controller, curve: Curves.linearToEaseOut);
428 _controller.forward();
429
430 /*return CupertinoUserInterfaceLevel(
431 data: CupertinoUserInterfaceLevelData.elevated,
432 child: Builder(builder: builder),
433 );*/
434
435 return animate ? FadeTransition(
436 opacity: _animation,
437 child: bottomSheet,
438 ) : bottomSheet;
439 }
440
441@override
442 Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
443 return Align(
444 alignment: Alignment.bottomCenter,
445 child: FractionalTranslation(
446 translation: _offsetTween.evaluate(_animation),
447 child: child,
448 ),
449 );
450 }
451}
452
453/// Shows a modal material design bottom sheet.
454///
455/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
456/// the user from interacting with the rest of the app.
457///
458/// A closely related widget is a persistent bottom sheet, which shows
459/// information that supplements the primary content of the app without
460/// preventing the use from interacting with the app. Persistent bottom sheets
461/// can be created and displayed with the [showBottomSheet] function or the
462/// [ScaffoldState.showBottomSheet] method.
463///
464/// The `context` argument is used to look up the [Navigator] and [Theme] for
465/// the bottom sheet. It is only used when the method is called. Its
466/// corresponding widget can be safely removed from the tree before the bottom
467/// sheet is closed.
468///
469/// The `isScrollControlled` parameter specifies whether this is a route for
470/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish
471/// to have a bottom sheet that has a scrollable child such as a [ListView] or
472/// a [GridView] and have the bottom sheet be draggable, you should set this
473/// parameter to true.
474///
475/// The `useRootNavigator` parameter ensures that the root navigator is used to
476/// display the [BottomSheet] when set to `true`. This is useful in the case
477/// that a modal [BottomSheet] needs to be displayed above all other content
478/// but the caller is inside another [Navigator].
479///
480/// The [isDismissible] parameter specifies whether the bottom sheet will be
481/// dismissed when user taps on the scrim.
482///
483/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
484/// parameters can be passed in to customize the appearance and behavior of
485/// modal bottom sheets.
486///
487/// Returns a `Future` that resolves to the value (if any) that was passed to
488/// [Navigator.pop] when the modal bottom sheet was closed.
489///
490/// See also:
491///
492/// * [BottomSheet], which becomes the parent of the widget returned by the
493/// function passed as the `builder` argument to [showModalBottomSheet].
494/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
495/// non-modal bottom sheets.
496/// * [DraggableScrollableSheet], which allows you to create a bottom sheet
497/// that grows and then becomes scrollable once it reaches its maximum size.
498/// * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet>
499Future<T> showModalBottomSheet<T>({
500 @required BuildContext context,
501 @required WidgetBuilder builder,
502 bool animate = false,
503 Color backgroundColor,
504 double elevation,
505 ShapeBorder shape,
506 Clip clipBehavior,
507 bool isScrollControlled = false,
508 bool useRootNavigator = false,
509 bool isDismissible = true,
510}) {
511 assert(context != null);
512 assert(builder != null);
513 assert(isScrollControlled != null);
514 assert(useRootNavigator != null);
515 assert(isDismissible != null);
516 assert(debugCheckHasMediaQuery(context));
517 assert(debugCheckHasMaterialLocalizations(context));
518
519 return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute<T>(
520 builder: builder,
521 animate: animate,
522 theme: Theme.of(context, shadowThemeOnly: true),
523 isScrollControlled: isScrollControlled,
524 barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
525 backgroundColor: backgroundColor,
526 elevation: elevation,
527 shape: shape,
528 clipBehavior: clipBehavior,
529 isDismissible: isDismissible,
530 ));
531}
532
533/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If
534/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet].
535///
536/// Returns a controller that can be used to close and otherwise manipulate the
537/// bottom sheet.
538///
539/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
540/// parameters can be passed in to customize the appearance and behavior of
541/// persistent bottom sheets.
542///
543/// To rebuild the bottom sheet (e.g. if it is stateful), call
544/// [PersistentBottomSheetController.setState] on the controller returned by
545/// this method.
546///
547/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing
548/// [ModalRoute] and a back button is added to the app bar of the [Scaffold]
549/// that closes the bottom sheet.
550///
551/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and
552/// does not add a back button to the enclosing Scaffold's app bar, use the
553/// [Scaffold.bottomSheet] constructor parameter.
554///
555/// A closely related widget is a modal bottom sheet, which is an alternative
556/// to a menu or a dialog and prevents the user from interacting with the rest
557/// of the app. Modal bottom sheets can be created and displayed with the
558/// [showModalBottomSheet] function.
559///
560/// The `context` argument is used to look up the [Scaffold] for the bottom
561/// sheet. It is only used when the method is called. Its corresponding widget
562/// can be safely removed from the tree before the bottom sheet is closed.
563///
564/// See also:
565///
566/// * [BottomSheet], which becomes the parent of the widget returned by the
567/// `builder`.
568/// * [showModalBottomSheet], which can be used to display a modal bottom
569/// sheet.
570/// * [Scaffold.of], for information about how to obtain the [BuildContext].
571/// * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
572PersistentBottomSheetController<T> showBottomSheet<T>({
573 @required BuildContext context,
574 @required WidgetBuilder builder,
575 Color backgroundColor,
576 double elevation,
577 ShapeBorder shape,
578 Clip clipBehavior,
579}) {
580 assert(context != null);
581 assert(builder != null);
582 assert(debugCheckHasScaffold(context));
583
584 return Scaffold.of(context).showBottomSheet<T>(
585 builder,
586 backgroundColor: backgroundColor,
587 elevation: elevation,
588 shape: shape,
589 clipBehavior: clipBehavior,
590 );
591}