// Copyright 2015 The Chromium 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 'dart:async'; import 'dart:collection'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'app_bar.dart'; import 'bottom_sheet.dart'; import 'button.dart'; import 'button_bar.dart'; import 'drawer.dart'; import 'flexible_space_bar.dart'; import 'material.dart'; import 'snack_bar.dart'; import 'theme.dart'; const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200); final Tween _kFloatingActionButtonTurnTween = new Tween(begin: -0.125, end: 0.0); enum _ScaffoldSlot { body, appBar, bottomSheet, snackBar, persistentFooter, bottomNavigationBar, floatingActionButton, drawer, endDrawer, statusBar, } class _ScaffoldLayout extends MultiChildLayoutDelegate { _ScaffoldLayout({ @required this.statusBarHeight, @required this.bottomPadding, @required this.endPadding, // for floating action button @required this.textDirection, }); final double statusBarHeight; final double bottomPadding; final double endPadding; final TextDirection textDirection; @override void performLayout(Size size) { final BoxConstraints looseConstraints = new BoxConstraints.loose(size); // This part of the layout has the same effect as putting the app bar and // body in a column and making the body flexible. What's different is that // in this case the app bar appears _after_ the body in the stacking order, // so the app bar's shadow is drawn on top of the body. final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width); final double bottom = math.max(0.0, size.height - bottomPadding); double contentTop = 0.0; double contentBottom = bottom; if (hasChild(_ScaffoldSlot.appBar)) { contentTop = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height; positionChild(_ScaffoldSlot.appBar, Offset.zero); } if (hasChild(_ScaffoldSlot.bottomNavigationBar)) { final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height; contentBottom -= bottomNavigationBarHeight; positionChild(_ScaffoldSlot.bottomNavigationBar, new Offset(0.0, contentBottom)); } if (hasChild(_ScaffoldSlot.persistentFooter)) { final BoxConstraints footerConstraints = new BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: math.max(0.0, contentBottom - contentTop), ); final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, footerConstraints).height; contentBottom -= persistentFooterHeight; positionChild(_ScaffoldSlot.persistentFooter, new Offset(0.0, contentBottom)); } if (hasChild(_ScaffoldSlot.body)) { final BoxConstraints bodyConstraints = new BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: math.max(0.0, contentBottom - contentTop), ); layoutChild(_ScaffoldSlot.body, bodyConstraints); positionChild(_ScaffoldSlot.body, new Offset(0.0, contentTop)); } // The BottomSheet and the SnackBar are anchored to the bottom of the parent, // they're as wide as the parent and are given their intrinsic height. The // only difference is that SnackBar appears on the top side of the // BottomNavigationBar while the BottomSheet is stacked on top of it. // // If all three elements are present then either the center of the FAB straddles // the top edge of the BottomSheet or the bottom of the FAB is // _kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB // the farthest above the bottom of the parent. If only the FAB is has a // non-zero height then it's inset from the parent's right and bottom edges // by _kFloatingActionButtonMargin. Size bottomSheetSize = Size.zero; Size snackBarSize = Size.zero; if (hasChild(_ScaffoldSlot.bottomSheet)) { final BoxConstraints bottomSheetConstraints = new BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: math.max(0.0, contentBottom - contentTop), ); bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, bottomSheetConstraints); positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, bottom - bottomSheetSize.height)); } if (hasChild(_ScaffoldSlot.snackBar)) { snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints); positionChild(_ScaffoldSlot.snackBar, new Offset(0.0, contentBottom - snackBarSize.height)); } if (hasChild(_ScaffoldSlot.floatingActionButton)) { final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints); double fabX; assert(textDirection != null); switch (textDirection) { case TextDirection.rtl: fabX = _kFloatingActionButtonMargin + endPadding; break; case TextDirection.ltr: fabX = size.width - fabSize.width - _kFloatingActionButtonMargin - endPadding; break; } double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin; if (snackBarSize.height > 0.0) fabY = math.min(fabY, contentBottom - snackBarSize.height - fabSize.height - _kFloatingActionButtonMargin); if (bottomSheetSize.height > 0.0) fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0); positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY)); } if (hasChild(_ScaffoldSlot.statusBar)) { layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: statusBarHeight)); positionChild(_ScaffoldSlot.statusBar, Offset.zero); } if (hasChild(_ScaffoldSlot.drawer)) { layoutChild(_ScaffoldSlot.drawer, new BoxConstraints.tight(size)); positionChild(_ScaffoldSlot.drawer, Offset.zero); } if (hasChild(_ScaffoldSlot.endDrawer)) { layoutChild(_ScaffoldSlot.endDrawer, new BoxConstraints.tight(size)); positionChild(_ScaffoldSlot.endDrawer, Offset.zero); } } @override bool shouldRelayout(_ScaffoldLayout oldDelegate) { return oldDelegate.statusBarHeight != statusBarHeight || oldDelegate.bottomPadding != bottomPadding || oldDelegate.endPadding != endPadding || oldDelegate.textDirection != textDirection; } } class _FloatingActionButtonTransition extends StatefulWidget { const _FloatingActionButtonTransition({ Key key, this.child, }) : super(key: key); final Widget child; @override _FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState(); } class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin { AnimationController _previousController; AnimationController _currentController; CurvedAnimation _previousAnimation; CurvedAnimation _currentAnimation; Widget _previousChild; @override void initState() { super.initState(); _previousController = new AnimationController( duration: _kFloatingActionButtonSegue, vsync: this, )..addStatusListener(_handleAnimationStatusChanged); _previousAnimation = new CurvedAnimation( parent: _previousController, curve: Curves.easeIn ); _currentController = new AnimationController( duration: _kFloatingActionButtonSegue, vsync: this, ); _currentAnimation = new CurvedAnimation( parent: _currentController, curve: Curves.easeIn ); // If we start out with a child, have the child appear fully visible instead // of animating in. if (widget.child != null) _currentController.value = 1.0; } @override void dispose() { _previousController.dispose(); _currentController.dispose(); super.dispose(); } @override void didUpdateWidget(_FloatingActionButtonTransition oldWidget) { super.didUpdateWidget(oldWidget); final bool oldChildIsNull = oldWidget.child == null; final bool newChildIsNull = widget.child == null; if (oldChildIsNull == newChildIsNull && oldWidget.child?.key == widget.child?.key) return; if (_previousController.status == AnimationStatus.dismissed) { final double currentValue = _currentController.value; if (currentValue == 0.0 || oldWidget.child == null) { // The current child hasn't started its entrance animation yet. We can // just skip directly to the new child's entrance. _previousChild = null; if (widget.child != null) _currentController.forward(); } else { // Otherwise, we need to copy the state from the current controller to // the previous controller and run an exit animation for the previous // widget before running the entrance animation for the new child. _previousChild = oldWidget.child; _previousController ..value = currentValue ..reverse(); _currentController.value = 0.0; } } } void _handleAnimationStatusChanged(AnimationStatus status) { setState(() { if (status == AnimationStatus.dismissed) { assert(_currentController.status == AnimationStatus.dismissed); if (widget.child != null) _currentController.forward(); } }); } @override Widget build(BuildContext context) { final List children = []; if (_previousAnimation.status != AnimationStatus.dismissed) { children.add(new ScaleTransition( scale: _previousAnimation, child: _previousChild, )); } if (_currentAnimation.status != AnimationStatus.dismissed) { children.add(new ScaleTransition( scale: _currentAnimation, child: new RotationTransition( turns: _kFloatingActionButtonTurnTween.animate(_currentAnimation), child: widget.child, ) )); } return new Stack(children: children); } } /// Implements the basic material design visual layout structure. /// /// This class provides APIs for showing drawers, snack bars, and bottom sheets. /// /// To display a snackbar or a persistent bottom sheet, obtain the /// [ScaffoldState] for the current [BuildContext] via [Scaffold.of] and use the /// [ScaffoldState.showSnackBar] and [ScaffoldState.showBottomSheet] functions. /// /// See also: /// /// * [AppBar], which is a horizontal bar typically shown at the top of an app /// using the [appBar] property. /// * [FloatingActionButton], which is a circular button typically shown in the /// bottom right corner of the app using the [floatingActionButton] property. /// * [Drawer], which is a vertical panel that is typically displayed to the /// left of the body (and often hidden on phones) using the [drawer] /// property. /// * [BottomNavigationBar], which is a horizontal array of buttons typically /// shown along the bottom of the app using the [bottomNavigationBar] /// property. /// * [SnackBar], which is a temporary notification typically shown near the /// bottom of the app using the [ScaffoldState.showSnackBar] method. /// * [BottomSheet], which is an overlay typically shown near the bottom of the /// app. A bottom sheet can either be persistent, in which case it is shown /// using the [ScaffoldState.showBottomSheet] method, or modal, in which case /// it is shown using the [showModalBottomSheet] function. /// * [ScaffoldState], which is the state associated with this widget. /// * class Scaffold extends StatefulWidget { /// Creates a visual scaffold for material design widgets. const Scaffold({ Key key, this.appBar, this.body, this.floatingActionButton, this.persistentFooterButtons, this.drawer, this.endDrawer, this.bottomNavigationBar, this.backgroundColor, this.resizeToAvoidBottomPadding: true, this.primary: true, }) : assert(primary != null), super(key: key); /// An app bar to display at the top of the scaffold. final PreferredSizeWidget appBar; /// The primary content of the scaffold. /// /// Displayed below the app bar and behind the [floatingActionButton] and /// [drawer]. To avoid the body being resized to avoid the window padding /// (e.g., from the onscreen keyboard), see [resizeToAvoidBottomPadding]. /// /// The widget in the body of the scaffold is positioned at the top-left of /// the available space between the app bar and the bottom of the scaffold. To /// center this widget instead, consider putting it in a [Center] widget and /// having that be the body. To expand this widget instead, consider /// putting it in a [SizedBox.expand]. /// /// If you have a column of widgets that should normally fit on the screen, /// but may overflow and would in such cases need to scroll, consider using a /// [ListView] as the body of the scaffold. This is also a good choice for /// the case where your body is a scrollable list. final Widget body; /// A button displayed floating above [body], in the bottom right corner. /// /// Typically a [FloatingActionButton]. final Widget floatingActionButton; /// A set of buttons that are displayed at the bottom of the scaffold. /// /// Typically this is a list of [FlatButton] widgets. These buttons are /// persistently visible, even if the [body] of the scaffold scrolls. /// /// These widgets will be wrapped in a [ButtonBar]. /// /// The [persistentFooterButtons] are rendered above the /// [bottomNavigationBar] but below the [body]. /// /// See also: /// /// * final List persistentFooterButtons; /// A panel displayed to the side of the [body], often hidden on mobile /// devices. Swipes in from either left-to-right ([TextDirection.ltr]) or /// right-to-left ([TextDirection.rtl]) /// /// In the uncommon case that you wish to open the drawer manually, use the /// [ScaffoldState.openDrawer] function. /// /// Typically a [Drawer]. final Widget drawer; /// A panel displayed to the side of the [body], often hidden on mobile /// devices. Swipes in from right-to-left ([TextDirection.ltr]) or /// left-to-right ([TextDirection.rtl]) /// /// In the uncommon case that you wish to open the drawer manually, use the /// [ScaffoldState.openDrawer] function. /// /// Typically a [Drawer]. final Widget endDrawer; /// The color of the [Material] widget that underlies the entire Scaffold. /// /// The theme's [ThemeData.scaffoldBackgroundColor] by default. final Color backgroundColor; /// A bottom navigation bar to display at the bottom of the scaffold. /// /// Snack bars slide from underneath the bottom navigation bar while bottom /// sheets are stacked on top. /// /// The [bottomNavigationBar] is rendered below the [persistentFooterButtons] /// and the [body]. final Widget bottomNavigationBar; /// Whether the [body] (and other floating widgets) should size themselves to /// avoid the window's bottom padding. /// /// For example, if there is an onscreen keyboard displayed above the /// scaffold, the body can be resized to avoid overlapping the keyboard, which /// prevents widgets inside the body from being obscured by the keyboard. /// /// Defaults to true. final bool resizeToAvoidBottomPadding; /// Whether this scaffold is being displayed at the top of the screen. /// /// If true then the height of the [appBar] will be extended by the height /// of the screen's status bar, i.e. the top padding for [MediaQuery]. /// /// The default value of this property, like the default value of /// [AppBar.primary], is true. final bool primary; /// The state from the closest instance of this class that encloses the given context. /// /// Typical usage is as follows: /// /// ```dart /// @override /// Widget build(BuildContext context) { /// return new RaisedButton( /// child: new Text('SHOW A SNACKBAR'), /// onPressed: () { /// Scaffold.of(context).showSnackBar(new SnackBar( /// content: new Text('Hello!'), /// )); /// }, /// ); /// } /// ``` /// /// When the [Scaffold] is actually created in the same `build` function, the /// `context` argument to the `build` function can't be used to find the /// [Scaffold] (since it's "above" the widget being returned). In such cases, /// the following technique with a [Builder] can be used to provide a new /// scope with a [BuildContext] that is "under" the [Scaffold]: /// /// ```dart /// @override /// Widget build(BuildContext context) { /// return new Scaffold( /// appBar: new AppBar( /// title: new Text('Demo') /// ), /// body: new Builder( /// // Create an inner BuildContext so that the onPressed methods /// // can refer to the Scaffold with Scaffold.of(). /// builder: (BuildContext context) { /// return new Center( /// child: new RaisedButton( /// child: new Text('SHOW A SNACKBAR'), /// onPressed: () { /// Scaffold.of(context).showSnackBar(new SnackBar( /// content: new Text('Hello!'), /// )); /// }, /// ), /// ); /// }, /// ), /// ); /// } /// ``` /// /// A more efficient solution is to split your build function into several /// widgets. This introduces a new context from which you can obtain the /// [Scaffold]. In this solution, you would have an outer widget that creates /// the [Scaffold] populated by instances of your new inner widgets, and then /// in these inner widgets you would use [Scaffold.of]. /// /// A less elegant but more expedient solution is assign a [GlobalKey] to the /// [Scaffold], then use the `key.currentState` property to obtain the /// [ScaffoldState] rather than using the [Scaffold.of] function. /// /// If there is no [Scaffold] in scope, then this will throw an exception. /// To return null if there is no [Scaffold], then pass `nullOk: true`. static ScaffoldState of(BuildContext context, { bool nullOk: false }) { assert(nullOk != null); assert(context != null); final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher()); if (nullOk || result != null) return result; throw new FlutterError( 'Scaffold.of() called with a context that does not contain a Scaffold.\n' 'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). ' 'This usually happens when the context provided is from the same StatefulWidget as that ' 'whose build function actually creates the Scaffold widget being sought.\n' 'There are several ways to avoid this problem. The simplest is to use a Builder to get a ' 'context that is "under" the Scaffold. For an example of this, please see the ' 'documentation for Scaffold.of():\n' ' https://docs.flutter.io/flutter/material/Scaffold/of.html\n' 'A more efficient solution is to split your build function into several widgets. This ' 'introduces a new context from which you can obtain the Scaffold. In this solution, ' 'you would have an outer widget that creates the Scaffold populated by instances of ' 'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n' 'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, ' 'then use the key.currentState property to obtain the ScaffoldState rather than ' 'using the Scaffold.of() function.\n' 'The context used was:\n' ' $context' ); } /// Whether the Scaffold that most tightly encloses the given context has a /// drawer. /// /// If this is being used during a build (for example to decide whether to /// show an "open drawer" button), set the `registerForUpdates` argument to /// true. This will then set up an [InheritedWidget] relationship with the /// [Scaffold] so that the client widget gets rebuilt whenever the [hasDrawer] /// value changes. /// /// See also: /// * [Scaffold.of], which provides access to the [ScaffoldState] object as a /// whole, from which you can show snackbars, bottom sheets, and so forth. static bool hasDrawer(BuildContext context, { bool registerForUpdates: true }) { assert(registerForUpdates != null); assert(context != null); if (registerForUpdates) { final _ScaffoldScope scaffold = context.inheritFromWidgetOfExactType(_ScaffoldScope); return scaffold?.hasDrawer ?? false; } else { final ScaffoldState scaffold = context.ancestorStateOfType(const TypeMatcher()); return scaffold?.hasDrawer ?? false; } } @override ScaffoldState createState() => new ScaffoldState(); } /// State for a [Scaffold]. /// /// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from /// the current [BuildContext] using [Scaffold.of]. class ScaffoldState extends State with TickerProviderStateMixin { // DRAWER API final GlobalKey _drawerKey = new GlobalKey(); final GlobalKey _endDrawerKey = new GlobalKey(); /// Whether this scaffold has a non-null [Scaffold.drawer]. bool get hasDrawer => widget.drawer != null; /// Whether this scaffold has a non-null [Scaffold.endDrawer]. bool get hasEndDrawer => widget.endDrawer != null; /// Opens the [Drawer] (if any). /// /// If the scaffold has a non-null [Scaffold.drawer], this function will cause /// the drawer to begin its entrance animation. /// /// Normally this is not needed since the [Scaffold] automatically shows an /// appropriate [IconButton], and handles the edge-swipe gesture, to show the /// drawer. /// /// To close the drawer once it is open, use [Navigator.pop]. /// /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. void openDrawer() { _drawerKey.currentState?.open(); } /// Opens the end side [Drawer] (if any). /// /// If the scaffold has a non-null [Scaffold.endDrawer], this function will cause /// the end side drawer to begin its entrance animation. /// /// Normally this is not needed since the [Scaffold] automatically shows an /// appropriate [IconButton], and handles the edge-swipe gesture, to show the /// drawer. /// /// To close the end side drawer once it is open, use [Navigator.pop]. /// /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. void openEndDrawer() { _endDrawerKey.currentState?.open(); } // SNACKBAR API final Queue> _snackBars = new Queue>(); AnimationController _snackBarController; Timer _snackBarTimer; /// Shows a [SnackBar] at the bottom of the scaffold. /// /// A scaffold can show at most one snack bar at a time. If this function is /// called while another snack bar is already visible, the given snack bar /// will be added to a queue and displayed after the earlier snack bars have /// closed. /// /// To control how long a [SnackBar] remains visible, use [SnackBar.duration]. /// /// To remove the [SnackBar] with an exit animation, use [hideCurrentSnackBar] /// or call [ScaffoldFeatureController.close] on the returned /// [ScaffoldFeatureController]. To remove a [SnackBar] suddenly (without an /// animation), use [removeCurrentSnackBar]. /// /// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. ScaffoldFeatureController showSnackBar(SnackBar snackbar) { _snackBarController ??= SnackBar.createAnimationController(vsync: this) ..addStatusListener(_handleSnackBarStatusChange); if (_snackBars.isEmpty) { assert(_snackBarController.isDismissed); _snackBarController.forward(); } ScaffoldFeatureController controller; controller = new ScaffoldFeatureController._( // We provide a fallback key so that if back-to-back snackbars happen to // match in structure, material ink splashes and highlights don't survive // from one to the next. snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()), new Completer(), () { assert(_snackBars.first == controller); hideCurrentSnackBar(reason: SnackBarClosedReason.hide); }, null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it ); setState(() { _snackBars.addLast(controller); }); return controller; } void _handleSnackBarStatusChange(AnimationStatus status) { switch (status) { case AnimationStatus.dismissed: assert(_snackBars.isNotEmpty); setState(() { _snackBars.removeFirst(); }); if (_snackBars.isNotEmpty) _snackBarController.forward(); break; case AnimationStatus.completed: setState(() { assert(_snackBarTimer == null); // build will create a new timer if necessary to dismiss the snack bar }); break; case AnimationStatus.forward: case AnimationStatus.reverse: break; } } /// Removes the current [SnackBar] (if any) immediately. /// /// The removed snack bar does not run its normal exit animation. If there are /// any queued snack bars, they begin their entrance animation immediately. void removeCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.remove }) { assert(reason != null); if (_snackBars.isEmpty) return; final Completer completer = _snackBars.first._completer; if (!completer.isCompleted) completer.complete(reason); _snackBarTimer?.cancel(); _snackBarTimer = null; _snackBarController.value = 0.0; } /// Removes the current [SnackBar] by running its normal exit animation. /// /// The closed completer is called after the animation is complete. void hideCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.hide }) { assert(reason != null); if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed) return; final Completer completer = _snackBars.first._completer; _snackBarController.reverse().then((Null _) { assert(mounted); if (!completer.isCompleted) completer.complete(reason); }); _snackBarTimer?.cancel(); _snackBarTimer = null; } // PERSISTENT BOTTOM SHEET API final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[]; PersistentBottomSheetController _currentBottomSheet; /// Shows a persistent material design bottom sheet. /// /// 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. /// /// 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. /// /// Returns a controller that can be used to close and otherwise manipulate the /// button sheet. /// /// See also: /// /// * [BottomSheet], which is the widget typically returned by the `builder`. /// * [showModalBottomSheet], which can be used to display a modal bottom /// sheet. /// * [Scaffold.of], for information about how to obtain the [ScaffoldState]. /// * PersistentBottomSheetController showBottomSheet(WidgetBuilder builder) { if (_currentBottomSheet != null) { _currentBottomSheet.close(); assert(_currentBottomSheet == null); } final Completer completer = new Completer(); final GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>(); final AnimationController controller = BottomSheet.createAnimationController(this) ..forward(); _PersistentBottomSheet bottomSheet; final LocalHistoryEntry entry = new LocalHistoryEntry( onRemove: () { assert(_currentBottomSheet._widget == bottomSheet); assert(bottomSheetKey.currentState != null); bottomSheetKey.currentState.close(); if (controller.status != AnimationStatus.dismissed) _dismissedBottomSheets.add(bottomSheet); setState(() { _currentBottomSheet = null; }); completer.complete(); } ); bottomSheet = new _PersistentBottomSheet( key: bottomSheetKey, animationController: controller, onClosing: () { assert(_currentBottomSheet._widget == bottomSheet); entry.remove(); }, onDismissed: () { if (_dismissedBottomSheets.contains(bottomSheet)) { bottomSheet.animationController.dispose(); setState(() { _dismissedBottomSheets.remove(bottomSheet); }); } }, builder: builder ); ModalRoute.of(context).addLocalHistoryEntry(entry); setState(() { _currentBottomSheet = new PersistentBottomSheetController._( bottomSheet, completer, entry.remove, (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); } ); }); return _currentBottomSheet; } // iOS FEATURES - status bar tap, back gesture // On iOS, tapping the status bar scrolls the app's primary scrollable to the // top. We implement this by providing a primary scroll controller and // scrolling it to the top when tapped. final ScrollController _primaryScrollController = new ScrollController(); void _handleStatusBarTap() { if (_primaryScrollController.hasClients) { _primaryScrollController.animateTo( 0.0, duration: const Duration(milliseconds: 300), curve: Curves.linear, // TODO(ianh): Use a more appropriate curve. ); } } // INTERNALS @override void dispose() { _snackBarController?.dispose(); _snackBarController = null; _snackBarTimer?.cancel(); _snackBarTimer = null; for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets) bottomSheet.animationController.dispose(); if (_currentBottomSheet != null) _currentBottomSheet._widget.animationController.dispose(); super.dispose(); } void _addIfNonNull(List children, Widget child, Object childId, { @required bool removeLeftPadding, @required bool removeTopPadding, @required bool removeRightPadding, bool removeBottomPadding, // defaults to widget.resizeToAvoidBottomPadding }) { if (child != null) { children.add( new LayoutId( id: childId, child: new MediaQuery.removePadding( context: context, removeLeft: removeLeftPadding, removeTop: removeTopPadding, removeRight: removeRightPadding, removeBottom: removeBottomPadding ?? widget.resizeToAvoidBottomPadding, child: child, ), ), ); } } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasDirectionality(context)); final EdgeInsets padding = MediaQuery.of(context).padding; final ThemeData themeData = Theme.of(context); final TextDirection textDirection = Directionality.of(context); if (_snackBars.isNotEmpty) { final ModalRoute route = ModalRoute.of(context); if (route == null || route.isCurrent) { if (_snackBarController.isCompleted && _snackBarTimer == null) _snackBarTimer = new Timer(_snackBars.first._widget.duration, () { assert(_snackBarController.status == AnimationStatus.forward || _snackBarController.status == AnimationStatus.completed); hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); }); } else { _snackBarTimer?.cancel(); _snackBarTimer = null; } } final List children = []; _addIfNonNull( children, widget.body, _ScaffoldSlot.body, removeLeftPadding: false, removeTopPadding: widget.appBar != null, removeRightPadding: false, ); if (widget.appBar != null) { final double topPadding = widget.primary ? padding.top : 0.0; final double extent = widget.appBar.preferredSize.height + topPadding; assert(extent >= 0.0 && extent.isFinite); _addIfNonNull( children, new ConstrainedBox( constraints: new BoxConstraints(maxHeight: extent), child: FlexibleSpaceBar.createSettings( currentExtent: extent, child: widget.appBar, ), ), _ScaffoldSlot.appBar, removeLeftPadding: false, removeTopPadding: false, removeRightPadding: false, removeBottomPadding: true, ); } if (_snackBars.isNotEmpty) { _addIfNonNull( children, _snackBars.first._widget, _ScaffoldSlot.snackBar, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, ); } if (widget.persistentFooterButtons != null) { _addIfNonNull( children, new Container( decoration: new BoxDecoration( border: new Border( top: new BorderSide( color: themeData.dividerColor ), ), ), child: new SafeArea( child: new ButtonTheme.bar( child: new ButtonBar( children: widget.persistentFooterButtons ), ), ), ), _ScaffoldSlot.persistentFooter, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, ); } if (widget.bottomNavigationBar != null) { _addIfNonNull( children, widget.bottomNavigationBar, _ScaffoldSlot.bottomNavigationBar, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, ); } if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) { final List bottomSheets = []; if (_dismissedBottomSheets.isNotEmpty) bottomSheets.addAll(_dismissedBottomSheets); if (_currentBottomSheet != null) bottomSheets.add(_currentBottomSheet._widget); final Widget stack = new Stack( children: bottomSheets, alignment: Alignment.bottomCenter, ); _addIfNonNull( children, stack, _ScaffoldSlot.bottomSheet, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, ); } _addIfNonNull( children, new _FloatingActionButtonTransition( child: widget.floatingActionButton, ), _ScaffoldSlot.floatingActionButton, removeLeftPadding: true, removeTopPadding: true, removeRightPadding: true, removeBottomPadding: true, ); if (themeData.platform == TargetPlatform.iOS) { _addIfNonNull( children, new GestureDetector( behavior: HitTestBehavior.opaque, onTap: _handleStatusBarTap, // iOS accessibility automatically adds scroll-to-top to the clock in the status bar excludeFromSemantics: true, ), _ScaffoldSlot.statusBar, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, removeBottomPadding: true, ); } if (widget.drawer != null) { assert(hasDrawer); _addIfNonNull( children, new DrawerController( key: _drawerKey, alignment: DrawerAlignment.start, child: widget.drawer, ), _ScaffoldSlot.drawer, // remove the side padding from the side we're not touching removeLeftPadding: textDirection == TextDirection.rtl, removeTopPadding: false, removeRightPadding: textDirection == TextDirection.ltr, removeBottomPadding: false, ); } if (widget.endDrawer != null) { assert(hasEndDrawer); _addIfNonNull( children, new DrawerController( key: _endDrawerKey, alignment: DrawerAlignment.end, child: widget.endDrawer, ), _ScaffoldSlot.endDrawer, // remove the side padding from the side we're not touching removeLeftPadding: textDirection == TextDirection.ltr, removeTopPadding: false, removeRightPadding: textDirection == TextDirection.rtl, removeBottomPadding: false, ); } double endPadding; switch (textDirection) { case TextDirection.rtl: endPadding = padding.left; break; case TextDirection.ltr: endPadding = padding.right; break; } assert(endPadding != null); return new _ScaffoldScope( hasDrawer: hasDrawer, child: new PrimaryScrollController( controller: _primaryScrollController, child: new Material( color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor, child: new CustomMultiChildLayout( children: children, delegate: new _ScaffoldLayout( statusBarHeight: padding.top, bottomPadding: widget.resizeToAvoidBottomPadding ? padding.bottom : 0.0, endPadding: endPadding, textDirection: textDirection, ), ), ), ), ); } } /// An interface for controlling a feature of a [Scaffold]. /// /// Commonly obtained from [ScaffoldState.showSnackBar] or [ScaffoldState.showBottomSheet]. class ScaffoldFeatureController { const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState); final T _widget; final Completer _completer; /// Completes when the feature controlled by this object is no longer visible. Future get closed => _completer.future; /// Remove the feature (e.g., bottom sheet or snack bar) from the scaffold. final VoidCallback close; /// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild. final StateSetter setState; } class _PersistentBottomSheet extends StatefulWidget { const _PersistentBottomSheet({ Key key, this.animationController, this.onClosing, this.onDismissed, this.builder }) : super(key: key); final AnimationController animationController; // we control it, but it must be disposed by whoever created it final VoidCallback onClosing; final VoidCallback onDismissed; final WidgetBuilder builder; @override _PersistentBottomSheetState createState() => new _PersistentBottomSheetState(); } class _PersistentBottomSheetState extends State<_PersistentBottomSheet> { @override void initState() { super.initState(); assert(widget.animationController.status == AnimationStatus.forward); widget.animationController.addStatusListener(_handleStatusChange); } @override void didUpdateWidget(_PersistentBottomSheet oldWidget) { super.didUpdateWidget(oldWidget); assert(widget.animationController == oldWidget.animationController); } void close() { widget.animationController.reverse(); } void _handleStatusChange(AnimationStatus status) { if (status == AnimationStatus.dismissed && widget.onDismissed != null) widget.onDismissed(); } @override Widget build(BuildContext context) { return new AnimatedBuilder( animation: widget.animationController, builder: (BuildContext context, Widget child) { return new Align( alignment: AlignmentDirectional.topStart, heightFactor: widget.animationController.value, child: child ); }, child: new Semantics( container: true, child: new BottomSheet( animationController: widget.animationController, onClosing: widget.onClosing, builder: widget.builder ) ) ); } } /// A [ScaffoldFeatureController] for persistent bottom sheets. /// /// This is the type of objects returned by [ScaffoldState.showBottomSheet]. class PersistentBottomSheetController extends ScaffoldFeatureController<_PersistentBottomSheet, T> { const PersistentBottomSheetController._( _PersistentBottomSheet widget, Completer completer, VoidCallback close, StateSetter setState ) : super._(widget, completer, close, setState); } class _ScaffoldScope extends InheritedWidget { const _ScaffoldScope({ @required this.hasDrawer, @required Widget child, }) : assert(hasDrawer != null), super(child: child); final bool hasDrawer; @override bool updateShouldNotify(_ScaffoldScope oldWidget) { return hasDrawer != oldWidget.hasDrawer; } }