// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // TODO(shihaohong): remove ignoring deprecated member use analysis // when Scaffold.shouldSnackBarIgnoreFABRect parameter is removed. // ignore_for_file: deprecated_member_use_from_same_package import 'dart:async'; import 'dart:collection'; import 'dart:math' as math; import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'app_bar.dart'; import 'bottom_sheet.dart'; import 'button_bar.dart'; import 'colors.dart'; import 'curves.dart'; import 'divider.dart'; import 'drawer.dart'; import 'flexible_space_bar.dart'; import 'floating_action_button.dart'; import 'floating_action_button_location.dart'; import 'material.dart'; import 'snack_bar.dart'; import 'snack_bar_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; // Examples can assume: // TabController tabController; // void setState(VoidCallback fn) { } // String appBarTitle; // int tabCount; // TickerProvider tickerProvider; const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat; const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling; const Curve _standardBottomSheetCurve = standardEasing; // When the top of the BottomSheet crosses this threshold, it will start to // shrink the FAB and show a scrim. const double _kBottomSheetDominatesPercentage = 0.3; const double _kMinBottomSheetScrimOpacity = 0.1; const double _kMaxBottomSheetScrimOpacity = 0.6; enum _ScaffoldSlot { body, appBar, bodyScrim, bottomSheet, snackBar, persistentFooter, bottomNavigationBar, floatingActionButton, drawer, endDrawer, statusBar, } /// The geometry of the [Scaffold] after all its contents have been laid out /// except the [FloatingActionButton]. /// /// The [Scaffold] passes this pre-layout geometry to its /// [FloatingActionButtonLocation], which produces an [Offset] that the /// [Scaffold] uses to position the [FloatingActionButton]. /// /// For a description of the [Scaffold]'s geometry after it has /// finished laying out, see the [ScaffoldGeometry]. @immutable class ScaffoldPrelayoutGeometry { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const ScaffoldPrelayoutGeometry({ @required this.bottomSheetSize, @required this.contentBottom, @required this.contentTop, @required this.floatingActionButtonSize, @required this.minInsets, @required this.scaffoldSize, @required this.snackBarSize, @required this.textDirection, }); /// The [Size] of [Scaffold.floatingActionButton]. /// /// If [Scaffold.floatingActionButton] is null, this will be [Size.zero]. final Size floatingActionButtonSize; /// The [Size] of the [Scaffold]'s [BottomSheet]. /// /// If the [Scaffold] is not currently showing a [BottomSheet], /// this will be [Size.zero]. final Size bottomSheetSize; /// The vertical distance from the Scaffold's origin to the bottom of /// [Scaffold.body]. /// /// This is useful in a [FloatingActionButtonLocation] designed to /// place the [FloatingActionButton] at the bottom of the screen, while /// keeping it above the [BottomSheet], the [Scaffold.bottomNavigationBar], /// or the keyboard. /// /// The [Scaffold.body] is laid out with respect to [minInsets] already. This /// means that a [FloatingActionButtonLocation] does not need to factor in /// [minInsets.bottom] when aligning a [FloatingActionButton] to /// [contentBottom]. final double contentBottom; /// The vertical distance from the [Scaffold]'s origin to the top of /// [Scaffold.body]. /// /// This is useful in a [FloatingActionButtonLocation] designed to /// place the [FloatingActionButton] at the top of the screen, while /// keeping it below the [Scaffold.appBar]. /// /// The [Scaffold.body] is laid out with respect to [minInsets] already. This /// means that a [FloatingActionButtonLocation] does not need to factor in /// [minInsets.top] when aligning a [FloatingActionButton] to [contentTop]. final double contentTop; /// The minimum padding to inset the [FloatingActionButton] by for it /// to remain visible. /// /// This value is the result of calling [MediaQuery.padding] in the /// [Scaffold]'s [BuildContext], /// and is useful for insetting the [FloatingActionButton] to avoid features like /// the system status bar or the keyboard. /// /// If [Scaffold.resizeToAvoidBottomInset] is set to false, [minInsets.bottom] /// will be 0.0. final EdgeInsets minInsets; /// The [Size] of the whole [Scaffold]. /// /// If the [Size] of the [Scaffold]'s contents is modified by values such as /// [Scaffold.resizeToAvoidBottomInset] or the keyboard opening, then the /// [scaffoldSize] will not reflect those changes. /// /// This means that [FloatingActionButtonLocation]s designed to reposition /// the [FloatingActionButton] based on events such as the keyboard popping /// up should use [minInsets] to make sure that the [FloatingActionButton] is /// inset by enough to remain visible. /// /// See [minInsets] and [MediaQuery.padding] for more information on the appropriate /// insets to apply. final Size scaffoldSize; /// The [Size] of the [Scaffold]'s [SnackBar]. /// /// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero]. final Size snackBarSize; /// The [TextDirection] of the [Scaffold]'s [BuildContext]. final TextDirection textDirection; } /// A snapshot of a transition between two [FloatingActionButtonLocation]s. /// /// [ScaffoldState] uses this to seamlessly change transition animations /// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition. @immutable class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation { const _TransitionSnapshotFabLocation(this.begin, this.end, this.animator, this.progress); final FloatingActionButtonLocation begin; final FloatingActionButtonLocation end; final FloatingActionButtonAnimator animator; final double progress; @override Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { return animator.getOffset( begin: begin.getOffset(scaffoldGeometry), end: end.getOffset(scaffoldGeometry), progress: progress, ); } @override String toString() { return '${objectRuntimeType(this, '_TransitionSnapshotFabLocation')}(begin: $begin, end: $end, progress: $progress)'; } } /// Geometry information for [Scaffold] components after layout is finished. /// /// To get a [ValueNotifier] for the scaffold geometry of a given /// [BuildContext], use [Scaffold.geometryOf]. /// /// The ScaffoldGeometry is only available during the paint phase, because /// its value is computed during the animation and layout phases prior to painting. /// /// For an example of using the [ScaffoldGeometry], see the [BottomAppBar], /// which uses the [ScaffoldGeometry] to paint a notch around the /// [FloatingActionButton]. /// /// For information about the [Scaffold]'s geometry that is used while laying /// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry]. @immutable class ScaffoldGeometry { /// Create an object that describes the geometry of a [Scaffold]. const ScaffoldGeometry({ this.bottomNavigationBarTop, this.floatingActionButtonArea, }); /// The distance from the [Scaffold]'s top edge to the top edge of the /// rectangle in which the [Scaffold.bottomNavigationBar] bar is laid out. /// /// Null if [Scaffold.bottomNavigationBar] is null. final double bottomNavigationBarTop; /// The [Scaffold.floatingActionButton]'s bounding rectangle. /// /// This is null when there is no floating action button showing. final Rect floatingActionButtonArea; ScaffoldGeometry _scaleFloatingActionButton(double scaleFactor) { if (scaleFactor == 1.0) return this; if (scaleFactor == 0.0) { return ScaffoldGeometry( bottomNavigationBarTop: bottomNavigationBarTop, ); } final Rect scaledButton = Rect.lerp( floatingActionButtonArea.center & Size.zero, floatingActionButtonArea, scaleFactor, ); return copyWith(floatingActionButtonArea: scaledButton); } /// Creates a copy of this [ScaffoldGeometry] but with the given fields replaced with /// the new values. ScaffoldGeometry copyWith({ double bottomNavigationBarTop, Rect floatingActionButtonArea, }) { return ScaffoldGeometry( bottomNavigationBarTop: bottomNavigationBarTop ?? this.bottomNavigationBarTop, floatingActionButtonArea: floatingActionButtonArea ?? this.floatingActionButtonArea, ); } } class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenable<ScaffoldGeometry> { _ScaffoldGeometryNotifier(this.geometry, this.context) : assert (context != null); final BuildContext context; double floatingActionButtonScale; ScaffoldGeometry geometry; @override ScaffoldGeometry get value { assert(() { final RenderObject renderObject = context.findRenderObject(); if (renderObject == null || !renderObject.owner.debugDoingPaint) throw FlutterError( 'Scaffold.geometryOf() must only be accessed during the paint phase.\n' 'The ScaffoldGeometry is only available during the paint phase, because ' 'its value is computed during the animation and layout phases prior to painting.' ); return true; }()); return geometry._scaleFloatingActionButton(floatingActionButtonScale); } void _updateWith({ double bottomNavigationBarTop, Rect floatingActionButtonArea, double floatingActionButtonScale, }) { this.floatingActionButtonScale = floatingActionButtonScale ?? this.floatingActionButtonScale; geometry = geometry.copyWith( bottomNavigationBarTop: bottomNavigationBarTop, floatingActionButtonArea: floatingActionButtonArea, ); notifyListeners(); } } // Used to communicate the height of the Scaffold's bottomNavigationBar and // persistentFooterButtons to the LayoutBuilder which builds the Scaffold's body. // // Scaffold expects a _BodyBoxConstraints to be passed to the _BodyBuilder // widget's LayoutBuilder, see _ScaffoldLayout.performLayout(). The BoxConstraints // methods that construct new BoxConstraints objects, like copyWith() have not // been overridden here because we expect the _BodyBoxConstraintsObject to be // passed along unmodified to the LayoutBuilder. If that changes in the future // then _BodyBuilder will assert. class _BodyBoxConstraints extends BoxConstraints { const _BodyBoxConstraints({ double minWidth = 0.0, double maxWidth = double.infinity, double minHeight = 0.0, double maxHeight = double.infinity, @required this.bottomWidgetsHeight, @required this.appBarHeight, }) : assert(bottomWidgetsHeight != null), assert(bottomWidgetsHeight >= 0), assert(appBarHeight != null), assert(appBarHeight >= 0), super(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight); final double bottomWidgetsHeight; final double appBarHeight; // RenderObject.layout() will only short-circuit its call to its performLayout // method if the new layout constraints are not == to the current constraints. // If the height of the bottom widgets has changed, even though the constraints' // min and max values have not, we still want performLayout to happen. @override bool operator ==(Object other) { if (super != other) return false; return other is _BodyBoxConstraints && other.bottomWidgetsHeight == bottomWidgetsHeight && other.appBarHeight == appBarHeight; } @override int get hashCode { return hashValues(super.hashCode, bottomWidgetsHeight, appBarHeight); } } // Used when Scaffold.extendBody is true to wrap the scaffold's body in a MediaQuery // whose padding accounts for the height of the bottomNavigationBar and/or the // persistentFooterButtons. // // The bottom widgets' height is passed along via the _BodyBoxConstraints parameter. // The constraints parameter is constructed in_ScaffoldLayout.performLayout(). class _BodyBuilder extends StatelessWidget { const _BodyBuilder({ Key key, @required this.extendBody, @required this.extendBodyBehindAppBar, @required this.body, }) : assert(extendBody != null), assert(extendBodyBehindAppBar != null), assert(body != null), super(key: key); final Widget body; final bool extendBody; final bool extendBodyBehindAppBar; @override Widget build(BuildContext context) { if (!extendBody && !extendBodyBehindAppBar) return body; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final _BodyBoxConstraints bodyConstraints = constraints as _BodyBoxConstraints; final MediaQueryData metrics = MediaQuery.of(context); final double bottom = extendBody ? math.max(metrics.padding.bottom, bodyConstraints.bottomWidgetsHeight) : metrics.padding.bottom; final double top = extendBodyBehindAppBar ? math.max(metrics.padding.top, bodyConstraints.appBarHeight) : metrics.padding.top; return MediaQuery( data: metrics.copyWith( padding: metrics.padding.copyWith( top: top, bottom: bottom, ), ), child: body, ); }, ); } } class _ScaffoldLayout extends MultiChildLayoutDelegate { _ScaffoldLayout({ @required this.minInsets, @required this.textDirection, @required this.geometryNotifier, // for floating action button @required this.previousFloatingActionButtonLocation, @required this.currentFloatingActionButtonLocation, @required this.floatingActionButtonMoveAnimationProgress, @required this.floatingActionButtonMotionAnimator, @required this.isSnackBarFloating, @required this.extendBody, @required this.extendBodyBehindAppBar, }) : assert(minInsets != null), assert(textDirection != null), assert(geometryNotifier != null), assert(previousFloatingActionButtonLocation != null), assert(currentFloatingActionButtonLocation != null), assert(extendBody != null), assert(extendBodyBehindAppBar != null); final bool extendBody; final bool extendBodyBehindAppBar; final EdgeInsets minInsets; final TextDirection textDirection; final _ScaffoldGeometryNotifier geometryNotifier; final FloatingActionButtonLocation previousFloatingActionButtonLocation; final FloatingActionButtonLocation currentFloatingActionButtonLocation; final double floatingActionButtonMoveAnimationProgress; final FloatingActionButtonAnimator floatingActionButtonMotionAnimator; final bool isSnackBarFloating; @override void performLayout(Size size) { final BoxConstraints looseConstraints = 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 = size.height; double contentTop = 0.0; double bottomWidgetsHeight = 0.0; double appBarHeight = 0.0; if (hasChild(_ScaffoldSlot.appBar)) { appBarHeight = layoutChild(_ScaffoldSlot.appBar, fullWidthConstraints).height; contentTop = extendBodyBehindAppBar ? 0.0 : appBarHeight; positionChild(_ScaffoldSlot.appBar, Offset.zero); } double bottomNavigationBarTop; if (hasChild(_ScaffoldSlot.bottomNavigationBar)) { final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height; bottomWidgetsHeight += bottomNavigationBarHeight; bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight); positionChild(_ScaffoldSlot.bottomNavigationBar, Offset(0.0, bottomNavigationBarTop)); } if (hasChild(_ScaffoldSlot.persistentFooter)) { final BoxConstraints footerConstraints = BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: math.max(0.0, bottom - bottomWidgetsHeight - contentTop), ); final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, footerConstraints).height; bottomWidgetsHeight += persistentFooterHeight; positionChild(_ScaffoldSlot.persistentFooter, Offset(0.0, math.max(0.0, bottom - bottomWidgetsHeight))); } // Set the content bottom to account for the greater of the height of any // bottom-anchored material widgets or of the keyboard or other // bottom-anchored system UI. final double contentBottom = math.max(0.0, bottom - math.max(minInsets.bottom, bottomWidgetsHeight)); if (hasChild(_ScaffoldSlot.body)) { double bodyMaxHeight = math.max(0.0, contentBottom - contentTop); if (extendBody) { bodyMaxHeight += bottomWidgetsHeight; bodyMaxHeight = bodyMaxHeight.clamp(0.0, looseConstraints.maxHeight - contentTop).toDouble(); assert(bodyMaxHeight <= math.max(0.0, looseConstraints.maxHeight - contentTop)); } final BoxConstraints bodyConstraints = _BodyBoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: bodyMaxHeight, bottomWidgetsHeight: extendBody ? bottomWidgetsHeight : 0.0, appBarHeight: appBarHeight, ); layoutChild(_ScaffoldSlot.body, bodyConstraints); positionChild(_ScaffoldSlot.body, 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.bodyScrim)) { final BoxConstraints bottomSheetScrimConstraints = BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: contentBottom, ); layoutChild(_ScaffoldSlot.bodyScrim, bottomSheetScrimConstraints); positionChild(_ScaffoldSlot.bodyScrim, Offset.zero); } // Set the size of the SnackBar early if the behavior is fixed so // the FAB can be positioned correctly. if (hasChild(_ScaffoldSlot.snackBar) && !isSnackBarFloating) { snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints); } if (hasChild(_ScaffoldSlot.bottomSheet)) { final BoxConstraints bottomSheetConstraints = BoxConstraints( maxWidth: fullWidthConstraints.maxWidth, maxHeight: math.max(0.0, contentBottom - contentTop), ); bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, bottomSheetConstraints); positionChild(_ScaffoldSlot.bottomSheet, Offset((size.width - bottomSheetSize.width) / 2.0, contentBottom - bottomSheetSize.height)); } Rect floatingActionButtonRect; if (hasChild(_ScaffoldSlot.floatingActionButton)) { final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints); // To account for the FAB position being changed, we'll animate between // the old and new positions. final ScaffoldPrelayoutGeometry currentGeometry = ScaffoldPrelayoutGeometry( bottomSheetSize: bottomSheetSize, contentBottom: contentBottom, contentTop: contentTop, floatingActionButtonSize: fabSize, minInsets: minInsets, scaffoldSize: size, snackBarSize: snackBarSize, textDirection: textDirection, ); final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry); final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry); final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset( begin: previousFabOffset, end: currentFabOffset, progress: floatingActionButtonMoveAnimationProgress, ); positionChild(_ScaffoldSlot.floatingActionButton, fabOffset); floatingActionButtonRect = fabOffset & fabSize; } if (hasChild(_ScaffoldSlot.snackBar)) { if (snackBarSize == Size.zero) { snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints); } double snackBarYOffsetBase; if (Scaffold.shouldSnackBarIgnoreFABRect) { if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating) snackBarYOffsetBase = floatingActionButtonRect.top; else snackBarYOffsetBase = contentBottom; } else { snackBarYOffsetBase = floatingActionButtonRect != null && isSnackBarFloating ? floatingActionButtonRect.top : contentBottom; } positionChild(_ScaffoldSlot.snackBar, Offset(0.0, snackBarYOffsetBase - snackBarSize.height)); } if (hasChild(_ScaffoldSlot.statusBar)) { layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top)); positionChild(_ScaffoldSlot.statusBar, Offset.zero); } if (hasChild(_ScaffoldSlot.drawer)) { layoutChild(_ScaffoldSlot.drawer, BoxConstraints.tight(size)); positionChild(_ScaffoldSlot.drawer, Offset.zero); } if (hasChild(_ScaffoldSlot.endDrawer)) { layoutChild(_ScaffoldSlot.endDrawer, BoxConstraints.tight(size)); positionChild(_ScaffoldSlot.endDrawer, Offset.zero); } geometryNotifier._updateWith( bottomNavigationBarTop: bottomNavigationBarTop, floatingActionButtonArea: floatingActionButtonRect, ); } @override bool shouldRelayout(_ScaffoldLayout oldDelegate) { return oldDelegate.minInsets != minInsets || oldDelegate.textDirection != textDirection || oldDelegate.floatingActionButtonMoveAnimationProgress != floatingActionButtonMoveAnimationProgress || oldDelegate.previousFloatingActionButtonLocation != previousFloatingActionButtonLocation || oldDelegate.currentFloatingActionButtonLocation != currentFloatingActionButtonLocation || oldDelegate.extendBody != extendBody || oldDelegate.extendBodyBehindAppBar != extendBodyBehindAppBar; } } /// Handler for scale and rotation animations in the [FloatingActionButton]. /// /// Currently, there are two types of [FloatingActionButton] animations: /// /// * Entrance/Exit animations, which this widget triggers /// when the [FloatingActionButton] is added, updated, or removed. /// * Motion animations, which are triggered by the [Scaffold] /// when its [FloatingActionButtonLocation] is updated. class _FloatingActionButtonTransition extends StatefulWidget { const _FloatingActionButtonTransition({ Key key, @required this.child, @required this.fabMoveAnimation, @required this.fabMotionAnimator, @required this.geometryNotifier, @required this.currentController, }) : assert(fabMoveAnimation != null), assert(fabMotionAnimator != null), assert(currentController != null), super(key: key); final Widget child; final Animation<double> fabMoveAnimation; final FloatingActionButtonAnimator fabMotionAnimator; final _ScaffoldGeometryNotifier geometryNotifier; /// Controls the current child widget.child as it exits. final AnimationController currentController; @override _FloatingActionButtonTransitionState createState() => _FloatingActionButtonTransitionState(); } class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin { // The animations applied to the Floating Action Button when it is entering or exiting. // Controls the previous widget.child as it exits. AnimationController _previousController; Animation<double> _previousScaleAnimation; Animation<double> _previousRotationAnimation; // The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations. Animation<double> _currentScaleAnimation; Animation<double> _extendedCurrentScaleAnimation; Animation<double> _currentRotationAnimation; Widget _previousChild; @override void initState() { super.initState(); _previousController = AnimationController( duration: kFloatingActionButtonSegue, vsync: this, )..addStatusListener(_handlePreviousAnimationStatusChanged); _updateAnimations(); if (widget.child != null) { // If we start out with a child, have the child appear fully visible instead // of animating in. widget.currentController.value = 1.0; } else { // If we start without a child we update the geometry object with a // floating action button scale of 0, as it is not showing on the screen. _updateGeometryScale(0.0); } } @override void dispose() { _previousController.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 (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || oldWidget.fabMoveAnimation != widget.fabMoveAnimation) { // Get the right scale and rotation animations to use for this widget. _updateAnimations(); } if (_previousController.status == AnimationStatus.dismissed) { final double currentValue = widget.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) widget.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(); widget.currentController.value = 0.0; } } } static final Animatable<double> _entranceTurnTween = Tween<double>( begin: 1.0 - kFloatingActionButtonTurnInterval, end: 1.0, ).chain(CurveTween(curve: Curves.easeIn)); void _updateAnimations() { // Get the animations for exit and entrance. final CurvedAnimation previousExitScaleAnimation = CurvedAnimation( parent: _previousController, curve: Curves.easeIn, ); final Animation<double> previousExitRotationAnimation = Tween<double>(begin: 1.0, end: 1.0).animate( CurvedAnimation( parent: _previousController, curve: Curves.easeIn, ), ); final CurvedAnimation currentEntranceScaleAnimation = CurvedAnimation( parent: widget.currentController, curve: Curves.easeIn, ); final Animation<double> currentEntranceRotationAnimation = widget.currentController.drive(_entranceTurnTween); // Get the animations for when the FAB is moving. final Animation<double> moveScaleAnimation = widget.fabMotionAnimator.getScaleAnimation(parent: widget.fabMoveAnimation); final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation); // Aggregate the animations. _previousScaleAnimation = AnimationMin<double>(moveScaleAnimation, previousExitScaleAnimation); _currentScaleAnimation = AnimationMin<double>(moveScaleAnimation, currentEntranceScaleAnimation); _extendedCurrentScaleAnimation = _currentScaleAnimation.drive(CurveTween(curve: const Interval(0.0, 0.1))); _previousRotationAnimation = TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation); _currentRotationAnimation = TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation); _currentScaleAnimation.addListener(_onProgressChanged); _previousScaleAnimation.addListener(_onProgressChanged); } void _handlePreviousAnimationStatusChanged(AnimationStatus status) { setState(() { if (status == AnimationStatus.dismissed) { assert(widget.currentController.status == AnimationStatus.dismissed); if (widget.child != null) widget.currentController.forward(); } }); } bool _isExtendedFloatingActionButton(Widget widget) { return widget is FloatingActionButton && widget.isExtended; } @override Widget build(BuildContext context) { return Stack( alignment: Alignment.centerRight, children: <Widget>[ if (_previousController.status != AnimationStatus.dismissed) if (_isExtendedFloatingActionButton(_previousChild)) FadeTransition( opacity: _previousScaleAnimation, child: _previousChild, ) else ScaleTransition( scale: _previousScaleAnimation, child: RotationTransition( turns: _previousRotationAnimation, child: _previousChild, ), ), if (_isExtendedFloatingActionButton(widget.child)) ScaleTransition( scale: _extendedCurrentScaleAnimation, child: FadeTransition( opacity: _currentScaleAnimation, child: widget.child, ), ) else ScaleTransition( scale: _currentScaleAnimation, child: RotationTransition( turns: _currentRotationAnimation, child: widget.child, ), ), ], ); } void _onProgressChanged() { _updateGeometryScale(math.max(_previousScaleAnimation.value, _currentScaleAnimation.value)); } void _updateGeometryScale(double scale) { widget.geometryNotifier._updateWith( floatingActionButtonScale: scale, ); } } /// 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. /// /// {@tool dartpad --template=stateful_widget_material} /// This example shows a [Scaffold] with a [body] and [FloatingActionButton]. /// The [body] is a [Text] placed in a [Center] in order to center the text /// within the [Scaffold]. The [FloatingActionButton] is connected to a /// callback that increments a counter. /// ///  /// /// ```dart /// int _count = 0; /// /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: const Text('Sample Code'), /// ), /// body: Center( /// child: Text('You have pressed the button $_count times.') /// ), /// floatingActionButton: FloatingActionButton( /// onPressed: () => setState(() => _count++), /// tooltip: 'Increment Counter', /// child: const Icon(Icons.add), /// ), /// ); /// } /// ``` /// {@end-tool} /// /// {@tool dartpad --template=stateful_widget_material} /// This example shows a [Scaffold] with a blueGrey [backgroundColor], [body] /// and [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in /// order to center the text within the [Scaffold]. The [FloatingActionButton] /// is connected to a callback that increments a counter. /// ///  /// /// ```dart /// int _count = 0; /// /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: const Text('Sample Code'), /// ), /// body: Center( /// child: Text('You have pressed the button $_count times.') /// ), /// backgroundColor: Colors.blueGrey.shade200, /// floatingActionButton: FloatingActionButton( /// onPressed: () => setState(() => _count++), /// tooltip: 'Increment Counter', /// child: const Icon(Icons.add), /// ), /// ); /// } /// ``` /// {@end-tool} /// /// {@tool dartpad --template=stateful_widget_material} /// This example shows a [Scaffold] with an [AppBar], a [BottomAppBar] and a /// [FloatingActionButton]. The [body] is a [Text] placed in a [Center] in order /// to center the text within the [Scaffold]. The [FloatingActionButton] is /// centered and docked within the [BottomAppBar] using /// [FloatingActionButtonLocation.centerDocked]. The [FloatingActionButton] is /// connected to a callback that increments a counter. /// ///  /// /// ```dart /// int _count = 0; /// /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: Text('Sample Code'), /// ), /// body: Center( /// child: Text('You have pressed the button $_count times.'), /// ), /// bottomNavigationBar: BottomAppBar( /// shape: const CircularNotchedRectangle(), /// child: Container(height: 50.0,), /// ), /// floatingActionButton: FloatingActionButton( /// onPressed: () => setState(() { /// _count++; /// }), /// tooltip: 'Increment Counter', /// child: Icon(Icons.add), /// ), /// floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, /// ); /// } /// ``` /// {@end-tool} /// /// ## Scaffold layout, the keyboard, and display "notches" /// /// The scaffold will expand to fill the available space. That usually /// means that it will occupy its entire window or device screen. When /// the device's keyboard appears the Scaffold's ancestor [MediaQuery] /// widget's [MediaQueryData.viewInsets] changes and the Scaffold will /// be rebuilt. By default the scaffold's [body] is resized to make /// room for the keyboard. To prevent the resize set /// [resizeToAvoidBottomInset] to false. In either case the focused /// widget will be scrolled into view if it's within a scrollable /// container. /// /// The [MediaQueryData.padding] value defines areas that might /// not be completely visible, like the display "notch" on the iPhone /// X. The scaffold's [body] is not inset by this padding value /// although an [appBar] or [bottomNavigationBar] will typically /// cause the body to avoid the padding. The [SafeArea] /// widget can be used within the scaffold's body to avoid areas /// like display notches. /// /// ## Troubleshooting /// /// ### Nested Scaffolds /// /// The Scaffold was designed to be the single top level container for /// a [MaterialApp] and it's typically not necessary to nest /// scaffolds. For example in a tabbed UI, where the /// [bottomNavigationBar] is a [TabBar] and the body is a /// [TabBarView], you might be tempted to make each tab bar view a /// scaffold with a differently titled AppBar. It would be better to add a /// listener to the [TabController] that updates the AppBar. /// /// {@tool snippet} /// Add a listener to the app's tab controller so that the [AppBar] title of the /// app's one and only scaffold is reset each time a new tab is selected. /// /// ```dart /// TabController(vsync: tickerProvider, length: tabCount)..addListener(() { /// if (!tabController.indexIsChanging) { /// setState(() { /// // Rebuild the enclosing scaffold with a new AppBar title /// appBarTitle = 'Tab ${tabController.index}'; /// }); /// } /// }) /// ``` /// {@end-tool} /// /// Although there are some use cases, like a presentation app that /// shows embedded flutter content, where nested scaffolds are /// appropriate, it's best to avoid nesting scaffolds. /// /// See also: /// /// * [AppBar], which is a horizontal bar typically shown at the top of an app /// using the [appBar] property. /// * [BottomAppBar], which is a horizontal bar typically shown at the bottom /// of an app using the [bottomNavigationBar] 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. /// * <https://material.io/design/layout/responsive-layout-grid.html> class Scaffold extends StatefulWidget { /// Creates a visual scaffold for material design widgets. const Scaffold({ Key key, this.appBar, this.body, this.floatingActionButton, this.floatingActionButtonLocation, this.floatingActionButtonAnimator, this.persistentFooterButtons, this.drawer, this.endDrawer, this.bottomNavigationBar, this.bottomSheet, this.backgroundColor, this.resizeToAvoidBottomPadding, this.resizeToAvoidBottomInset, this.primary = true, this.drawerDragStartBehavior = DragStartBehavior.start, this.extendBody = false, this.extendBodyBehindAppBar = false, this.drawerScrimColor, this.drawerEdgeDragWidth, this.drawerEnableOpenDragGesture = true, this.endDrawerEnableOpenDragGesture = true, }) : assert(primary != null), assert(extendBody != null), assert(extendBodyBehindAppBar != null), assert(drawerDragStartBehavior != null), super(key: key); /// If true, and [bottomNavigationBar] or [persistentFooterButtons] /// is specified, then the [body] extends to the bottom of the Scaffold, /// instead of only extending to the top of the [bottomNavigationBar] /// or the [persistentFooterButtons]. /// /// If true, a [MediaQuery] widget whose bottom padding matches the /// the height of the [bottomNavigationBar] will be added above the /// scaffold's [body]. /// /// This property is often useful when the [bottomNavigationBar] has /// a non-rectangular shape, like [CircularNotchedRectangle], which /// adds a [FloatingActionButton] sized notch to the top edge of the bar. /// In this case specifying `extendBody: true` ensures that that scaffold's /// body will be visible through the bottom navigation bar's notch. /// /// See also: /// /// * [extendBodyBehindAppBar], which extends the height of the body /// to the top of the scaffold. final bool extendBody; /// If true, and an [appBar] is specified, then the height of the [body] is /// extended to include the height of the app bar and the top of the body /// is aligned with the top of the app bar. /// /// This is useful if the app bar's [AppBar.backgroundColor] is not /// completely opaque. /// /// This property is false by default. It must not be null. /// /// See also: /// /// * [extendBody], which extends the height of the body to the bottom /// of the scaffold. final bool extendBodyBehindAppBar; /// An app bar to display at the top of the scaffold. final PreferredSizeWidget appBar; /// The primary content of the scaffold. /// /// Displayed below the [appBar], above the bottom of the ambient /// [MediaQuery]'s [MediaQueryData.viewInsets], and behind the /// [floatingActionButton] and [drawer]. If [resizeToAvoidBottomInset] is /// false then the body is not resized when the onscreen keyboard appears, /// i.e. it is not inset by `viewInsets.bottom`. /// /// 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; /// Responsible for determining where the [floatingActionButton] should go. /// /// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat]. final FloatingActionButtonLocation floatingActionButtonLocation; /// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation]. /// /// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling]. final FloatingActionButtonAnimator floatingActionButtonAnimator; /// 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]. final List<Widget> 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]) /// /// Typically a [Drawer]. /// /// To open the drawer, use the [ScaffoldState.openDrawer] function. /// /// To close the drawer, use [Navigator.pop]. /// /// {@tool dartpad --template=stateful_widget_material} /// To disable the drawer edge swipe, set the /// [Scaffold.drawerEnableOpenDragGesture] to false. Then, use /// [ScaffoldState.openDrawer] to open the drawer and [Navigator.pop] to close /// it. /// /// ```dart /// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); /// /// void _openDrawer() { /// _scaffoldKey.currentState.openDrawer(); /// } /// /// void _closeDrawer() { /// Navigator.of(context).pop(); /// } /// /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// key: _scaffoldKey, /// appBar: AppBar(title: const Text('Drawer Demo')), /// body: Center( /// child: RaisedButton( /// onPressed: _openDrawer, /// child: const Text('Open Drawer'), /// ), /// ), /// drawer: Drawer( /// child: Center( /// child: Column( /// mainAxisAlignment: MainAxisAlignment.center, /// children: <Widget>[ /// const Text('This is the Drawer'), /// RaisedButton( /// onPressed: _closeDrawer, /// child: const Text('Close Drawer'), /// ), /// ], /// ), /// ), /// ), /// // Disable opening the drawer with a swipe gesture. /// drawerEnableOpenDragGesture: false, /// ); /// } /// ``` /// {@end-tool} 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]) /// /// Typically a [Drawer]. /// /// To open the drawer, use the [ScaffoldState.openEndDrawer] function. /// /// To close the drawer, use [Navigator.pop]. /// /// {@tool dartpad --template=stateful_widget_material} /// To disable the drawer edge swipe, set the /// [Scaffold.endDrawerEnableOpenDragGesture] to false. Then, use /// [ScaffoldState.openEndDrawer] to open the drawer and [Navigator.pop] to /// close it. /// /// ```dart /// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); /// /// void _openEndDrawer() { /// _scaffoldKey.currentState.openEndDrawer(); /// } /// /// void _closeEndDrawer() { /// Navigator.of(context).pop(); /// } /// /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// key: _scaffoldKey, /// appBar: AppBar(title: Text('Drawer Demo')), /// body: Center( /// child: RaisedButton( /// onPressed: _openEndDrawer, /// child: Text('Open End Drawer'), /// ), /// ), /// endDrawer: Drawer( /// child: Center( /// child: Column( /// mainAxisAlignment: MainAxisAlignment.center, /// children: <Widget>[ /// const Text('This is the Drawer'), /// RaisedButton( /// onPressed: _closeEndDrawer, /// child: const Text('Close Drawer'), /// ), /// ], /// ), /// ), /// ), /// // Disable opening the end drawer with a swipe gesture. /// endDrawerEnableOpenDragGesture: false, /// ); /// } /// ``` /// {@end-tool} final Widget endDrawer; /// The color to use for the scrim that obscures primary content while a drawer is open. /// /// By default, the color is [Colors.black54] final Color drawerScrimColor; /// 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; /// The persistent bottom sheet to display. /// /// 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. /// /// Unlike the persistent bottom sheet displayed by [showBottomSheet] /// this bottom sheet is not a [LocalHistoryEntry] and cannot be dismissed /// with the scaffold appbar's back button. /// /// If a persistent bottom sheet created with [showBottomSheet] is already /// visible, it must be closed before building the Scaffold with a new /// [bottomSheet]. /// /// The value of [bottomSheet] can be any widget at all. It's unlikely to /// actually be a [BottomSheet], which is used by the implementations of /// [showBottomSheet] and [showModalBottomSheet]. Typically it's a widget /// that includes [Material]. /// /// See also: /// /// * [showBottomSheet], which displays a bottom sheet as a route that can /// be dismissed with the scaffold's back button. /// * [showModalBottomSheet], which displays a modal bottom sheet. final Widget bottomSheet; /// This flag is deprecated, please use [resizeToAvoidBottomInset] /// instead. /// /// Originally the name referred [MediaQueryData.padding]. Now it refers /// [MediaQueryData.viewInsets], so using [resizeToAvoidBottomInset] /// should be clearer to readers. @Deprecated( 'Use resizeToAvoidBottomInset to specify if the body should resize when the keyboard appears. ' 'This feature was deprecated after v1.1.9.' ) final bool resizeToAvoidBottomPadding; /// If true the [body] and the scaffold's floating widgets should size /// themselves to avoid the onscreen keyboard whose height is defined by the /// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property. /// /// 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 resizeToAvoidBottomInset; /// 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; /// {@macro flutter.material.drawer.dragStartBehavior} final DragStartBehavior drawerDragStartBehavior; /// The width of the area within which a horizontal swipe will open the /// drawer. /// /// By default, the value used is 20.0 added to the padding edge of /// `MediaQuery.of(context).padding` that corresponds to [alignment]. /// This ensures that the drag area for notched devices is not obscured. For /// example, if `TextDirection.of(context)` is set to [TextDirection.ltr], /// 20.0 will be added to `MediaQuery.of(context).padding.left`. final double drawerEdgeDragWidth; /// Determines if the [Scaffold.drawer] can be opened with a drag /// gesture. /// /// By default, the drag gesture is enabled. final bool drawerEnableOpenDragGesture; /// Determines if the [Scaffold.endDrawer] can be opened with a /// drag gesture. /// /// By default, the drag gesture is enabled. final bool endDrawerEnableOpenDragGesture; /// This flag is deprecated and fixes and issue with incorrect clipping /// and positioning of the [SnackBar] set to [SnackBarBehavior.floating]. @Deprecated( 'This property controls whether to clip and position the snackbar as ' 'if there is always a floating action button, even if one is not present. ' 'It exists to provide backwards compatibility to ease migrations, and will ' 'eventually be removed. ' 'This feature was deprecated after v1.15.3.' ) static bool shouldSnackBarIgnoreFABRect = false; /// The state from the closest instance of this class that encloses the given context. /// /// {@tool dartpad --template=freeform} /// Typical usage of the [Scaffold.of] function is to call it from within the /// `build` method of a child of a [Scaffold]. /// /// ```dart imports /// import 'package:flutter/material.dart'; /// ``` /// /// ```dart main /// void main() => runApp(MyApp()); /// ``` /// /// ```dart preamble /// class MyApp extends StatelessWidget { /// // This widget is the root of your application. /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// title: 'Flutter Code Sample for Scaffold.of.', /// theme: ThemeData( /// primarySwatch: Colors.blue, /// ), /// home: Scaffold( /// body: MyScaffoldBody(), /// appBar: AppBar(title: Text('Scaffold.of Example')), /// ), /// color: Colors.white, /// ); /// } /// } /// ``` /// /// ```dart /// class MyScaffoldBody extends StatelessWidget { /// @override /// Widget build(BuildContext context) { /// return Center( /// child: RaisedButton( /// child: Text('SHOW A SNACKBAR'), /// onPressed: () { /// Scaffold.of(context).showSnackBar( /// SnackBar( /// content: Text('Have a snack!'), /// ), /// ); /// }, /// ), /// ); /// } /// } /// ``` /// {@end-tool} /// /// {@tool dartpad --template=stateless_widget_material} /// 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 the widget /// tree). 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 /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: Text('Demo') /// ), /// body: Builder( /// // Create an inner BuildContext so that the onPressed methods /// // can refer to the Scaffold with Scaffold.of(). /// builder: (BuildContext context) { /// return Center( /// child: RaisedButton( /// child: Text('SHOW A SNACKBAR'), /// onPressed: () { /// Scaffold.of(context).showSnackBar(SnackBar( /// content: Text('Have a snack!'), /// )); /// }, /// ), /// ); /// }, /// ), /// ); /// } /// ``` /// {@end-tool} /// /// 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.findAncestorStateOfType<ScaffoldState>(); if (nullOk || result != null) return result; throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'Scaffold.of() called with a context that does not contain a Scaffold.' ), ErrorDescription( '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.' ), ErrorHint( '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://api.flutter.dev/flutter/material/Scaffold/of.html' ), ErrorHint( '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.' ), context.describeElement('The context used was') ]); } /// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest /// [Scaffold] ancestor of the given context. /// /// The [ValueListenable.value] is only available at paint time. /// /// Notifications are guaranteed to be sent before the first paint pass /// with the new geometry, but there is no guarantee whether a build or /// layout passes are going to happen between the notification and the next /// paint pass. /// /// The closest [Scaffold] ancestor for the context might change, e.g when /// an element is moved from one scaffold to another. For [StatefulWidget]s /// using this listenable, a change of the [Scaffold] ancestor will /// trigger a [State.didChangeDependencies]. /// /// A typical pattern for listening to the scaffold geometry would be to /// call [Scaffold.geometryOf] in [State.didChangeDependencies], compare the /// return value with the previous listenable, if it has changed, unregister /// the listener, and register a listener to the new [ScaffoldGeometry] /// listenable. static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) { final _ScaffoldScope scaffoldScope = context.dependOnInheritedWidgetOfExactType<_ScaffoldScope>(); if (scaffoldScope == null) throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'Scaffold.geometryOf() called with a context that does not contain a Scaffold.' ), ErrorDescription( 'This usually happens when the context provided is from the same StatefulWidget as that ' 'whose build function actually creates the Scaffold widget being sought.' ), ErrorHint( '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://api.flutter.dev/flutter/material/Scaffold/of.html' ), ErrorHint( '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.geometryOf().', ), context.describeElement('The context used was') ]); return scaffoldScope.geometryNotifier; } /// 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.dependOnInheritedWidgetOfExactType<_ScaffoldScope>(); return scaffold?.hasDrawer ?? false; } else { final ScaffoldState scaffold = context.findAncestorStateOfType<ScaffoldState>(); return scaffold?.hasDrawer ?? false; } } @override ScaffoldState createState() => 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<Scaffold> with TickerProviderStateMixin { // DRAWER API final GlobalKey<DrawerControllerState> _drawerKey = GlobalKey<DrawerControllerState>(); final GlobalKey<DrawerControllerState> _endDrawerKey = GlobalKey<DrawerControllerState>(); /// Whether this scaffold has a non-null [Scaffold.appBar]. bool get hasAppBar => widget.appBar != null; /// 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; /// Whether this scaffold has a non-null [Scaffold.floatingActionButton]. bool get hasFloatingActionButton => widget.floatingActionButton != null; double _appBarMaxHeight; /// The max height the [Scaffold.appBar] uses. /// /// This is based on the appBar preferred height plus the top padding. double get appBarMaxHeight => _appBarMaxHeight; bool _drawerOpened = false; bool _endDrawerOpened = false; /// Whether the [Scaffold.drawer] is opened. /// /// See also: /// /// * [ScaffoldState.openDrawer], which opens the [Scaffold.drawer] of a /// [Scaffold]. bool get isDrawerOpen => _drawerOpened; /// Whether the [Scaffold.endDrawer] is opened. /// /// See also: /// /// * [ScaffoldState.openEndDrawer], which opens the [Scaffold.endDrawer] of /// a [Scaffold]. bool get isEndDrawerOpen => _endDrawerOpened; void _drawerOpenedCallback(bool isOpened) { setState(() { _drawerOpened = isOpened; }); } void _endDrawerOpenedCallback(bool isOpened) { setState(() { _endDrawerOpened = isOpened; }); } /// 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() { if (_endDrawerKey.currentState != null && _endDrawerOpened) _endDrawerKey.currentState.close(); _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() { if (_drawerKey.currentState != null && _drawerOpened) _drawerKey.currentState.close(); _endDrawerKey.currentState?.open(); } // SNACKBAR API final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); AnimationController _snackBarController; Timer _snackBarTimer; bool _accessibleNavigation; /// 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<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) { _snackBarController ??= SnackBar.createAnimationController(vsync: this) ..addStatusListener(_handleSnackBarStatusChange); if (_snackBars.isEmpty) { assert(_snackBarController.isDismissed); _snackBarController.forward(); } ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller; controller = ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._( // 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: UniqueKey()), Completer<SnackBarClosedReason>(), () { 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<SnackBarClosedReason> 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 MediaQueryData mediaQuery = MediaQuery.of(context); final Completer<SnackBarClosedReason> completer = _snackBars.first._completer; if (mediaQuery.accessibleNavigation) { _snackBarController.value = 0.0; completer.complete(reason); } else { _snackBarController.reverse().then<void>((void value) { assert(mounted); if (!completer.isCompleted) completer.complete(reason); }); } _snackBarTimer?.cancel(); _snackBarTimer = null; } // PERSISTENT BOTTOM SHEET API // Contains bottom sheets that may still be animating out of view. // Important if the app/user takes an action that could repeatedly show a // bottom sheet. final List<_StandardBottomSheet> _dismissedBottomSheets = <_StandardBottomSheet>[]; PersistentBottomSheetController<dynamic> _currentBottomSheet; void _maybeBuildPersistentBottomSheet() { if (widget.bottomSheet != null && _currentBottomSheet == null) { // The new _currentBottomSheet is not a local history entry so a "back" button // will not be added to the Scaffold's appbar and the bottom sheet will not // support drag or swipe to dismiss. final AnimationController animationController = BottomSheet.createAnimationController(this)..value = 1.0; LocalHistoryEntry _persistentSheetHistoryEntry; bool _persistentBottomSheetExtentChanged(DraggableScrollableNotification notification) { if (notification.extent > notification.initialExtent) { if (_persistentSheetHistoryEntry == null) { _persistentSheetHistoryEntry = LocalHistoryEntry(onRemove: () { if (notification.extent > notification.initialExtent) { DraggableScrollableActuator.reset(notification.context); } showBodyScrim(false, 0.0); _floatingActionButtonVisibilityValue = 1.0; _persistentSheetHistoryEntry = null; }); ModalRoute.of(context).addLocalHistoryEntry(_persistentSheetHistoryEntry); } } else if (_persistentSheetHistoryEntry != null) { ModalRoute.of(context).removeLocalHistoryEntry(_persistentSheetHistoryEntry); } return false; } _currentBottomSheet = _buildBottomSheet<void>( (BuildContext context) { return NotificationListener<DraggableScrollableNotification>( onNotification: _persistentBottomSheetExtentChanged, child: DraggableScrollableActuator( child: widget.bottomSheet, ), ); }, true, animationController: animationController, ); } } void _closeCurrentBottomSheet() { if (_currentBottomSheet != null) { if (!_currentBottomSheet._isLocalHistoryEntry) { _currentBottomSheet.close(); } assert(() { _currentBottomSheet?._completer?.future?.whenComplete(() { assert(_currentBottomSheet == null); }); return true; }()); } } PersistentBottomSheetController<T> _buildBottomSheet<T>( WidgetBuilder builder, bool isPersistent, { AnimationController animationController, Color backgroundColor, double elevation, ShapeBorder shape, Clip clipBehavior, }) { assert(() { if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) { throw FlutterError( 'Scaffold.bottomSheet cannot be specified while a bottom sheet ' 'displayed with showBottomSheet() is still visible.\n' 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().' ); } return true; }()); final Completer<T> completer = Completer<T>(); final GlobalKey<_StandardBottomSheetState> bottomSheetKey = GlobalKey<_StandardBottomSheetState>(); _StandardBottomSheet bottomSheet; bool removedEntry = false; void _removeCurrentBottomSheet() { removedEntry = true; if (_currentBottomSheet == null) { return; } assert(_currentBottomSheet._widget == bottomSheet); assert(bottomSheetKey.currentState != null); _showFloatingActionButton(); void _closed(void value) { setState(() { _currentBottomSheet = null; }); if (animationController.status != AnimationStatus.dismissed) { _dismissedBottomSheets.add(bottomSheet); } completer.complete(); } final Future<void> closing = bottomSheetKey.currentState.close(); if (closing != null) { closing.then(_closed); } else { _closed(null); } } final LocalHistoryEntry entry = isPersistent ? null : LocalHistoryEntry(onRemove: () { if (!removedEntry) { _removeCurrentBottomSheet(); } }); bottomSheet = _StandardBottomSheet( key: bottomSheetKey, animationController: animationController, enableDrag: !isPersistent, onClosing: () { if (_currentBottomSheet == null) { return; } assert(_currentBottomSheet._widget == bottomSheet); if (!isPersistent && !removedEntry) { assert(entry != null); entry.remove(); removedEntry = true; } }, onDismissed: () { if (_dismissedBottomSheets.contains(bottomSheet)) { setState(() { _dismissedBottomSheets.remove(bottomSheet); }); } }, builder: builder, isPersistent: isPersistent, backgroundColor: backgroundColor, elevation: elevation, shape: shape, clipBehavior: clipBehavior, ); if (!isPersistent) ModalRoute.of(context).addLocalHistoryEntry(entry); return PersistentBottomSheetController<T>._( bottomSheet, completer, entry != null ? entry.remove : _removeCurrentBottomSheet, (VoidCallback fn) { bottomSheetKey.currentState?.setState(fn); }, !isPersistent, ); } /// Shows a material design bottom sheet in the nearest [Scaffold]. To show /// a persistent bottom sheet, use the [Scaffold.bottomSheet]. /// /// Returns a controller that can be used to close and otherwise manipulate the /// bottom sheet. /// /// To rebuild the bottom sheet (e.g. if it is stateful), call /// [PersistentBottomSheetController.setState] on the controller returned by /// this method. /// /// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing /// [ModalRoute] and a back button is added to the app bar of the [Scaffold] /// that closes the bottom sheet. /// /// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and /// does not add a back button to the enclosing Scaffold's app bar, use the /// [Scaffold.bottomSheet] constructor parameter. /// /// A 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. /// /// {@tool dartpad --template=stateless_widget_scaffold} /// /// This example demonstrates how to use `showBottomSheet` to display a /// bottom sheet when a user taps a button. It also demonstrates how to /// close a bottom sheet using the Navigator. /// /// ```dart /// Widget build(BuildContext context) { /// return Center( /// child: RaisedButton( /// child: const Text('showBottomSheet'), /// onPressed: () { /// Scaffold.of(context).showBottomSheet<void>( /// (BuildContext context) { /// return Container( /// height: 200, /// color: Colors.amber, /// child: Center( /// child: Column( /// mainAxisAlignment: MainAxisAlignment.center, /// mainAxisSize: MainAxisSize.min, /// children: <Widget>[ /// const Text('BottomSheet'), /// RaisedButton( /// child: const Text('Close BottomSheet'), /// onPressed: () => Navigator.pop(context), /// ) /// ], /// ), /// ), /// ); /// }, /// ); /// }, /// ), /// ); /// } /// ``` /// {@end-tool} /// See also: /// /// * [BottomSheet], which becomes the parent of the widget returned by the /// `builder`. /// * [showBottomSheet], which calls this method given a [BuildContext]. /// * [showModalBottomSheet], which can be used to display a modal bottom /// sheet. /// * [Scaffold.of], for information about how to obtain the [ScaffoldState]. /// * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet> PersistentBottomSheetController<T> showBottomSheet<T>( WidgetBuilder builder, { Color backgroundColor, double elevation, ShapeBorder shape, Clip clipBehavior, }) { assert(() { if (widget.bottomSheet != null) { throw FlutterError( 'Scaffold.bottomSheet cannot be specified while a bottom sheet ' 'displayed with showBottomSheet() is still visible.\n' 'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().' ); } return true; }()); assert(debugCheckHasMediaQuery(context)); _closeCurrentBottomSheet(); final AnimationController controller = BottomSheet.createAnimationController(this)..forward(); setState(() { _currentBottomSheet = _buildBottomSheet<T>( builder, false, animationController: controller, backgroundColor: backgroundColor, elevation: elevation, shape: shape, clipBehavior: clipBehavior, ); }); return _currentBottomSheet as PersistentBottomSheetController<T>; } // Floating Action Button API AnimationController _floatingActionButtonMoveController; FloatingActionButtonAnimator _floatingActionButtonAnimator; FloatingActionButtonLocation _previousFloatingActionButtonLocation; FloatingActionButtonLocation _floatingActionButtonLocation; AnimationController _floatingActionButtonVisibilityController; /// Gets the current value of the visibility animation for the /// [Scaffold.floatingActionButton]. double get _floatingActionButtonVisibilityValue => _floatingActionButtonVisibilityController.value; /// Sets the current value of the visibility animation for the /// [Scaffold.floatingActionButton]. This value must not be null. set _floatingActionButtonVisibilityValue(double newValue) { assert(newValue != null); _floatingActionButtonVisibilityController.value = newValue.clamp( _floatingActionButtonVisibilityController.lowerBound, _floatingActionButtonVisibilityController.upperBound, ) as double; } /// Shows the [Scaffold.floatingActionButton]. TickerFuture _showFloatingActionButton() { return _floatingActionButtonVisibilityController.forward(); } // Moves the Floating Action Button to the new Floating Action Button Location. void _moveFloatingActionButton(final FloatingActionButtonLocation newLocation) { FloatingActionButtonLocation previousLocation = _floatingActionButtonLocation; double restartAnimationFrom = 0.0; // If the Floating Action Button is moving right now, we need to start from a snapshot of the current transition. if (_floatingActionButtonMoveController.isAnimating) { previousLocation = _TransitionSnapshotFabLocation(_previousFloatingActionButtonLocation, _floatingActionButtonLocation, _floatingActionButtonAnimator, _floatingActionButtonMoveController.value); restartAnimationFrom = _floatingActionButtonAnimator.getAnimationRestart(_floatingActionButtonMoveController.value); } setState(() { _previousFloatingActionButtonLocation = previousLocation; _floatingActionButtonLocation = newLocation; }); // Animate the motion even when the fab is null so that if the exit animation is running, // the old fab will start the motion transition while it exits instead of jumping to the // new position. _floatingActionButtonMoveController.forward(from: restartAnimationFrom); } // 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 = 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 _ScaffoldGeometryNotifier _geometryNotifier; // Backwards compatibility for deprecated resizeToAvoidBottomPadding property bool get _resizeToAvoidBottomInset { // ignore: deprecated_member_use_from_same_package return widget.resizeToAvoidBottomInset ?? widget.resizeToAvoidBottomPadding ?? true; } @override void initState() { super.initState(); _geometryNotifier = _ScaffoldGeometryNotifier(const ScaffoldGeometry(), context); _floatingActionButtonLocation = widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation; _floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator; _previousFloatingActionButtonLocation = _floatingActionButtonLocation; _floatingActionButtonMoveController = AnimationController( vsync: this, lowerBound: 0.0, upperBound: 1.0, value: 1.0, duration: kFloatingActionButtonSegue * 2, ); _floatingActionButtonVisibilityController = AnimationController( duration: kFloatingActionButtonSegue, vsync: this, ); } @override void didUpdateWidget(Scaffold oldWidget) { // Update the Floating Action Button Animator, and then schedule the Floating Action Button for repositioning. if (widget.floatingActionButtonAnimator != oldWidget.floatingActionButtonAnimator) { _floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator; } if (widget.floatingActionButtonLocation != oldWidget.floatingActionButtonLocation) { _moveFloatingActionButton(widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation); } if (widget.bottomSheet != oldWidget.bottomSheet) { assert(() { if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed ' 'with showBottomSheet() is still visible.' ), ErrorHint( 'Use the PersistentBottomSheetController ' 'returned by showBottomSheet() to close the old bottom sheet before creating ' 'a Scaffold with a (non null) bottomSheet.' ), ]); } return true; }()); _closeCurrentBottomSheet(); _maybeBuildPersistentBottomSheet(); } super.didUpdateWidget(oldWidget); } @override void didChangeDependencies() { final MediaQueryData mediaQuery = MediaQuery.of(context); // If we transition from accessible navigation to non-accessible navigation // and there is a SnackBar that would have timed out that has already // completed its timer, dismiss that SnackBar. If the timer hasn't finished // yet, let it timeout as normal. if (_accessibleNavigation == true && !mediaQuery.accessibleNavigation && _snackBarTimer != null && !_snackBarTimer.isActive) { hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); } _accessibleNavigation = mediaQuery.accessibleNavigation; _maybeBuildPersistentBottomSheet(); super.didChangeDependencies(); } @override void dispose() { _snackBarController?.dispose(); _snackBarTimer?.cancel(); _snackBarTimer = null; _geometryNotifier.dispose(); for (final _StandardBottomSheet bottomSheet in _dismissedBottomSheets) { bottomSheet.animationController?.dispose(); } if (_currentBottomSheet != null) { _currentBottomSheet._widget.animationController?.dispose(); } _floatingActionButtonMoveController.dispose(); _floatingActionButtonVisibilityController.dispose(); super.dispose(); } void _addIfNonNull( List<LayoutId> children, Widget child, Object childId, { @required bool removeLeftPadding, @required bool removeTopPadding, @required bool removeRightPadding, @required bool removeBottomPadding, bool removeBottomInset = false, bool maintainBottomViewPadding = false, }) { MediaQueryData data = MediaQuery.of(context).removePadding( removeLeft: removeLeftPadding, removeTop: removeTopPadding, removeRight: removeRightPadding, removeBottom: removeBottomPadding, ); if (removeBottomInset) data = data.removeViewInsets(removeBottom: true); if (maintainBottomViewPadding && data.viewInsets.bottom != 0.0) { data = data.copyWith( padding: data.padding.copyWith(bottom: data.viewPadding.bottom) ); } if (child != null) { children.add( LayoutId( id: childId, child: MediaQuery(data: data, child: child), ), ); } } void _buildEndDrawer(List<LayoutId> children, TextDirection textDirection) { if (widget.endDrawer != null) { assert(hasEndDrawer); _addIfNonNull( children, DrawerController( key: _endDrawerKey, alignment: DrawerAlignment.end, child: widget.endDrawer, drawerCallback: _endDrawerOpenedCallback, dragStartBehavior: widget.drawerDragStartBehavior, scrimColor: widget.drawerScrimColor, edgeDragWidth: widget.drawerEdgeDragWidth, enableOpenDragGesture: widget.endDrawerEnableOpenDragGesture, ), _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, ); } } void _buildDrawer(List<LayoutId> children, TextDirection textDirection) { if (widget.drawer != null) { assert(hasDrawer); _addIfNonNull( children, DrawerController( key: _drawerKey, alignment: DrawerAlignment.start, child: widget.drawer, drawerCallback: _drawerOpenedCallback, dragStartBehavior: widget.drawerDragStartBehavior, scrimColor: widget.drawerScrimColor, edgeDragWidth: widget.drawerEdgeDragWidth, enableOpenDragGesture: widget.drawerEnableOpenDragGesture, ), _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, ); } } bool _showBodyScrim = false; Color _bodyScrimColor = Colors.black; /// Whether to show a [ModalBarrier] over the body of the scaffold. /// /// The `value` parameter must not be null. void showBodyScrim(bool value, double opacity) { assert(value != null); if (_showBodyScrim == value && _bodyScrimColor.opacity == opacity) { return; } setState(() { _showBodyScrim = value; _bodyScrimColor = Colors.black.withOpacity(opacity); }); } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasDirectionality(context)); final MediaQueryData mediaQuery = MediaQuery.of(context); final ThemeData themeData = Theme.of(context); final TextDirection textDirection = Directionality.of(context); _accessibleNavigation = mediaQuery.accessibleNavigation; if (_snackBars.isNotEmpty) { final ModalRoute<dynamic> route = ModalRoute.of(context); if (route == null || route.isCurrent) { if (_snackBarController.isCompleted && _snackBarTimer == null) { final SnackBar snackBar = _snackBars.first._widget; _snackBarTimer = Timer(snackBar.duration, () { assert(_snackBarController.status == AnimationStatus.forward || _snackBarController.status == AnimationStatus.completed); // Look up MediaQuery again in case the setting changed. final MediaQueryData mediaQuery = MediaQuery.of(context); if (mediaQuery.accessibleNavigation && snackBar.action != null) return; hideCurrentSnackBar(reason: SnackBarClosedReason.timeout); }); } } else { _snackBarTimer?.cancel(); _snackBarTimer = null; } } final List<LayoutId> children = <LayoutId>[]; _addIfNonNull( children, widget.body == null ? null : _BodyBuilder( extendBody: widget.extendBody, extendBodyBehindAppBar: widget.extendBodyBehindAppBar, body: widget.body, ), _ScaffoldSlot.body, removeLeftPadding: false, removeTopPadding: widget.appBar != null, removeRightPadding: false, removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null, removeBottomInset: _resizeToAvoidBottomInset, ); if (_showBodyScrim) { _addIfNonNull( children, ModalBarrier( dismissible: false, color: _bodyScrimColor, ), _ScaffoldSlot.bodyScrim, removeLeftPadding: true, removeTopPadding: true, removeRightPadding: true, removeBottomPadding: true, ); } if (widget.appBar != null) { final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0; _appBarMaxHeight = widget.appBar.preferredSize.height + topPadding; assert(_appBarMaxHeight >= 0.0 && _appBarMaxHeight.isFinite); _addIfNonNull( children, ConstrainedBox( constraints: BoxConstraints(maxHeight: _appBarMaxHeight), child: FlexibleSpaceBar.createSettings( currentExtent: _appBarMaxHeight, child: widget.appBar, ), ), _ScaffoldSlot.appBar, removeLeftPadding: false, removeTopPadding: false, removeRightPadding: false, removeBottomPadding: true, ); } bool isSnackBarFloating = false; if (_snackBars.isNotEmpty) { final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior ?? themeData.snackBarTheme.behavior ?? SnackBarBehavior.fixed; isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating; _addIfNonNull( children, _snackBars.first._widget, _ScaffoldSlot.snackBar, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null, maintainBottomViewPadding: !_resizeToAvoidBottomInset, ); } if (widget.persistentFooterButtons != null) { _addIfNonNull( children, Container( decoration: BoxDecoration( border: Border( top: Divider.createBorderSide(context, width: 1.0), ), ), child: SafeArea( top: false, child: ButtonBar( children: widget.persistentFooterButtons, ), ), ), _ScaffoldSlot.persistentFooter, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, removeBottomPadding: false, maintainBottomViewPadding: !_resizeToAvoidBottomInset, ); } if (widget.bottomNavigationBar != null) { _addIfNonNull( children, widget.bottomNavigationBar, _ScaffoldSlot.bottomNavigationBar, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, removeBottomPadding: false, maintainBottomViewPadding: !_resizeToAvoidBottomInset, ); } if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) { final Widget stack = Stack( alignment: Alignment.bottomCenter, children: <Widget>[ ..._dismissedBottomSheets, if (_currentBottomSheet != null) _currentBottomSheet._widget, ], ); _addIfNonNull( children, stack, _ScaffoldSlot.bottomSheet, removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, removeBottomPadding: _resizeToAvoidBottomInset, ); } _addIfNonNull( children, _FloatingActionButtonTransition( child: widget.floatingActionButton, fabMoveAnimation: _floatingActionButtonMoveController, fabMotionAnimator: _floatingActionButtonAnimator, geometryNotifier: _geometryNotifier, currentController: _floatingActionButtonVisibilityController, ), _ScaffoldSlot.floatingActionButton, removeLeftPadding: true, removeTopPadding: true, removeRightPadding: true, removeBottomPadding: true, ); switch (themeData.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: _addIfNonNull( children, 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, ); break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: break; } if (_endDrawerOpened) { _buildDrawer(children, textDirection); _buildEndDrawer(children, textDirection); } else { _buildEndDrawer(children, textDirection); _buildDrawer(children, textDirection); } // The minimum insets for contents of the Scaffold to keep visible. final EdgeInsets minInsets = mediaQuery.padding.copyWith( bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0, ); // extendBody locked when keyboard is open final bool _extendBody = minInsets.bottom <= 0 && widget.extendBody; return _ScaffoldScope( hasDrawer: hasDrawer, geometryNotifier: _geometryNotifier, child: PrimaryScrollController( controller: _primaryScrollController, child: Material( color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor, child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget child) { return CustomMultiChildLayout( children: children, delegate: _ScaffoldLayout( extendBody: _extendBody, extendBodyBehindAppBar: widget.extendBodyBehindAppBar, minInsets: minInsets, currentFloatingActionButtonLocation: _floatingActionButtonLocation, floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value, floatingActionButtonMotionAnimator: _floatingActionButtonAnimator, geometryNotifier: _geometryNotifier, previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation, textDirection: textDirection, isSnackBarFloating: isSnackBarFloating, ), ); }), ), ), ); } } /// An interface for controlling a feature of a [Scaffold]. /// /// Commonly obtained from [ScaffoldState.showSnackBar] or [ScaffoldState.showBottomSheet]. class ScaffoldFeatureController<T extends Widget, U> { const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState); final T _widget; final Completer<U> _completer; /// Completes when the feature controlled by this object is no longer visible. Future<U> 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; } // TODO(guidezpl): Look into making this public. A copy of this class is in bottom_sheet.dart, for now. /// A curve that progresses linearly until a specified [startingPoint], at which /// point [curve] will begin. Unlike [Interval], [curve] will not start at zero, /// but will use [startingPoint] as the Y position. /// /// For example, if [startingPoint] is set to `0.5`, and [curve] is set to /// [Curves.easeOut], then the bottom-left quarter of the curve will be a /// straight line, and the top-right quarter will contain the entire contents of /// [Curves.easeOut]. /// /// This is useful in situations where a widget must track the user's finger /// (which requires a linear animation), and afterwards can be flung using a /// curve specified with the [curve] argument, after the finger is released. In /// such a case, the value of [startingPoint] would be the progress of the /// animation at the time when the finger was released. /// /// The [startingPoint] and [curve] arguments must not be null. class _BottomSheetSuspendedCurve extends ParametricCurve<double> { /// Creates a suspended curve. const _BottomSheetSuspendedCurve( this.startingPoint, { this.curve = Curves.easeOutCubic, }) : assert(startingPoint != null), assert(curve != null); /// The progress value at which [curve] should begin. /// /// This defaults to [Curves.easeOutCubic]. final double startingPoint; /// The curve to use when [startingPoint] is reached. final Curve curve; @override double transform(double t) { assert(t >= 0.0 && t <= 1.0); assert(startingPoint >= 0.0 && startingPoint <= 1.0); if (t < startingPoint) { return t; } if (t == 1.0) { return t; } final double curveProgress = (t - startingPoint) / (1 - startingPoint); final double transformed = curve.transform(curveProgress); return lerpDouble(startingPoint, 1, transformed); } @override String toString() { return '${describeIdentity(this)}($startingPoint, $curve)'; } } class _StandardBottomSheet extends StatefulWidget { const _StandardBottomSheet({ Key key, this.animationController, this.enableDrag = true, this.onClosing, this.onDismissed, this.builder, this.isPersistent = false, this.backgroundColor, this.elevation, this.shape, this.clipBehavior, }) : super(key: key); final AnimationController animationController; // we control it, but it must be disposed by whoever created it. final bool enableDrag; final VoidCallback onClosing; final VoidCallback onDismissed; final WidgetBuilder builder; final bool isPersistent; final Color backgroundColor; final double elevation; final ShapeBorder shape; final Clip clipBehavior; @override _StandardBottomSheetState createState() => _StandardBottomSheetState(); } class _StandardBottomSheetState extends State<_StandardBottomSheet> { ParametricCurve<double> animationCurve = _standardBottomSheetCurve; @override void initState() { super.initState(); assert(widget.animationController != null); assert(widget.animationController.status == AnimationStatus.forward || widget.animationController.status == AnimationStatus.completed); widget.animationController.addStatusListener(_handleStatusChange); } @override void didUpdateWidget(_StandardBottomSheet oldWidget) { super.didUpdateWidget(oldWidget); assert(widget.animationController == oldWidget.animationController); } Future<void> close() { assert(widget.animationController != null); widget.animationController.reverse(); if (widget.onClosing != null) { widget.onClosing(); } return null; } void _handleDragStart(DragStartDetails details) { // Allow the bottom sheet to track the user's finger accurately. animationCurve = Curves.linear; } void _handleDragEnd(DragEndDetails details, { bool isClosing }) { // Allow the bottom sheet to animate smoothly from its current position. animationCurve = _BottomSheetSuspendedCurve( widget.animationController.value, curve: _standardBottomSheetCurve, ); } void _handleStatusChange(AnimationStatus status) { if (status == AnimationStatus.dismissed && widget.onDismissed != null) { widget.onDismissed(); } } bool extentChanged(DraggableScrollableNotification notification) { final double extentRemaining = 1.0 - notification.extent; final ScaffoldState scaffold = Scaffold.of(context); if (extentRemaining < _kBottomSheetDominatesPercentage) { scaffold._floatingActionButtonVisibilityValue = extentRemaining * _kBottomSheetDominatesPercentage * 10; scaffold.showBodyScrim(true, math.max( _kMinBottomSheetScrimOpacity, _kMaxBottomSheetScrimOpacity - scaffold._floatingActionButtonVisibilityValue, )); } else { scaffold._floatingActionButtonVisibilityValue = 1.0; scaffold.showBodyScrim(false, 0.0); } // If the Scaffold.bottomSheet != null, we're a persistent bottom sheet. if (notification.extent == notification.minExtent && scaffold.widget.bottomSheet == null) { close(); } return false; } Widget _wrapBottomSheet(Widget bottomSheet) { return Semantics( container: true, onDismiss: close, child: NotificationListener<DraggableScrollableNotification>( onNotification: extentChanged, child: bottomSheet, ), ); } @override Widget build(BuildContext context) { if (widget.animationController != null) { return AnimatedBuilder( animation: widget.animationController, builder: (BuildContext context, Widget child) { return Align( alignment: AlignmentDirectional.topStart, heightFactor: animationCurve.transform(widget.animationController.value), child: child, ); }, child: _wrapBottomSheet( BottomSheet( animationController: widget.animationController, enableDrag: widget.enableDrag, onDragStart: _handleDragStart, onDragEnd: _handleDragEnd, onClosing: widget.onClosing, builder: widget.builder, backgroundColor: widget.backgroundColor, elevation: widget.elevation, shape: widget.shape, clipBehavior: widget.clipBehavior, ), ), ); } return _wrapBottomSheet( BottomSheet( onClosing: widget.onClosing, builder: widget.builder, backgroundColor: widget.backgroundColor, ), ); } } /// A [ScaffoldFeatureController] for standard bottom sheets. /// /// This is the type of objects returned by [ScaffoldState.showBottomSheet]. /// /// This controller is used to display both standard and persistent bottom /// sheets. A bottom sheet is only persistent if it is set as the /// [Scaffold.bottomSheet]. class PersistentBottomSheetController<T> extends ScaffoldFeatureController<_StandardBottomSheet, T> { const PersistentBottomSheetController._( _StandardBottomSheet widget, Completer<T> completer, VoidCallback close, StateSetter setState, this._isLocalHistoryEntry, ) : super._(widget, completer, close, setState); final bool _isLocalHistoryEntry; } class _ScaffoldScope extends InheritedWidget { const _ScaffoldScope({ Key key, @required this.hasDrawer, @required this.geometryNotifier, @required Widget child, }) : assert(hasDrawer != null), super(key: key, child: child); final bool hasDrawer; final _ScaffoldGeometryNotifier geometryNotifier; @override bool updateShouldNotify(_ScaffoldScope oldWidget) { return hasDrawer != oldWidget.hasDrawer; } }