| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'scaffold.dart'; |
| |
| // TODO(hmuller): should be device dependent. |
| /// The margin that a [FloatingActionButton] should leave between it and the |
| /// edge of the screen. |
| /// |
| /// [FloatingActionButtonLocation.endFloat] uses this to set the appropriate margin |
| /// between the [FloatingActionButton] and the end of the screen. |
| const double kFloatingActionButtonMargin = 16.0; |
| |
| /// The amount of time the [FloatingActionButton] takes to transition in or out. |
| /// |
| /// The [Scaffold] uses this to set the duration of [FloatingActionButton] |
| /// motion, entrance, and exit animations. |
| const Duration kFloatingActionButtonSegue = Duration(milliseconds: 200); |
| |
| /// The fraction of a circle the [FloatingActionButton] should turn when it enters. |
| /// |
| /// Its value corresponds to 0.125 of a full circle, equivalent to 45 degrees or pi/4 radians. |
| const double kFloatingActionButtonTurnInterval = 0.125; |
| |
| /// An object that defines a position for the [FloatingActionButton] |
| /// based on the [Scaffold]'s [ScaffoldPrelayoutGeometry]. |
| /// |
| /// Flutter provides [FloatingActionButtonLocation]s for the common |
| /// [FloatingActionButton] placements in Material Design applications. These |
| /// locations are available as static members of this class. |
| /// |
| /// See also: |
| /// |
| /// * [FloatingActionButton], which is a circular button typically shown in the |
| /// bottom right corner of the app. |
| /// * [FloatingActionButtonAnimator], which is used to animate the |
| /// [Scaffold.floatingActionButton] from one [FloatingActionButtonLocation] to |
| /// another. |
| /// * [ScaffoldPrelayoutGeometry], the geometry that |
| /// [FloatingActionButtonLocation]s use to position the [FloatingActionButton]. |
| abstract class FloatingActionButtonLocation { |
| /// Abstract const constructor. This constructor enables subclasses to provide |
| /// const constructors so that they can be used in const expressions. |
| const FloatingActionButtonLocation(); |
| |
| /// End-aligned [FloatingActionButton], floating at the bottom of the screen. |
| /// |
| /// This is the default alignment of [FloatingActionButton]s in Material applications. |
| static const FloatingActionButtonLocation endFloat = _EndFloatFloatingActionButtonLocation(); |
| |
| /// Centered [FloatingActionButton], floating at the bottom of the screen. |
| static const FloatingActionButtonLocation centerFloat = _CenterFloatFloatingActionButtonLocation(); |
| |
| /// End-aligned [FloatingActionButton], floating over the |
| /// [Scaffold.bottomNavigationBar] so that the center of the floating |
| /// action button lines up with the top of the bottom navigation bar. |
| /// |
| /// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar], |
| /// the bottom app bar can include a "notch" in its shape that accommodates |
| /// the overlapping floating action button. |
| /// |
| /// This is unlikely to be a useful location for apps that lack a bottom |
| /// navigation bar. |
| static const FloatingActionButtonLocation endDocked = _EndDockedFloatingActionButtonLocation(); |
| |
| /// Center-aligned [FloatingActionButton], floating over the |
| /// [Scaffold.bottomNavigationBar] so that the center of the floating |
| /// action button lines up with the top of the bottom navigation bar. |
| /// |
| /// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar], |
| /// the bottom app bar can include a "notch" in its shape that accommodates |
| /// the overlapping floating action button. |
| /// |
| /// This is unlikely to be a useful location for apps that lack a bottom |
| /// navigation bar. |
| static const FloatingActionButtonLocation centerDocked = _CenterDockedFloatingActionButtonLocation(); |
| |
| /// Start-aligned [FloatingActionButton], floating over the transition between |
| /// the [Scaffold.appBar] and the [Scaffold.body]. |
| /// |
| /// To align a floating action button with [FloatingActionButton.mini] set to |
| /// true with [CircleAvatar]s in the [ListTile.leading] slots of [ListTile]s |
| /// in a [ListView] in the [Scaffold.body], consider using [miniStartTop]. |
| /// |
| /// This is unlikely to be a useful location for apps that lack a top [AppBar] |
| /// or that use a [SliverAppBar] in the scaffold body itself. |
| static const FloatingActionButtonLocation startTop = _StartTopFloatingActionButtonLocation(); |
| |
| /// Start-aligned [FloatingActionButton], floating over the transition between |
| /// the [Scaffold.appBar] and the [Scaffold.body], optimized for mini floating |
| /// action buttons. |
| /// |
| /// This is intended to be used with [FloatingActionButton.mini] set to true, |
| /// so that the floating action button appears to align with [CircleAvatar]s |
| /// in the [ListTile.leading] slot of a [ListTile] in a [ListView] in the |
| /// [Scaffold.body]. |
| /// |
| /// This is unlikely to be a useful location for apps that lack a top [AppBar] |
| /// or that use a [SliverAppBar] in the scaffold body itself. |
| static const FloatingActionButtonLocation miniStartTop = _MiniStartTopFloatingActionButtonLocation(); |
| |
| /// End-aligned [FloatingActionButton], floating over the transition between |
| /// the [Scaffold.appBar] and the [Scaffold.body]. |
| /// |
| /// This is unlikely to be a useful location for apps that lack a top [AppBar] |
| /// or that use a [SliverAppBar] in the scaffold body itself. |
| static const FloatingActionButtonLocation endTop = _EndTopFloatingActionButtonLocation(); |
| |
| /// Places the [FloatingActionButton] based on the [Scaffold]'s layout. |
| /// |
| /// This uses a [ScaffoldPrelayoutGeometry], which the [Scaffold] constructs |
| /// during its layout phase after it has laid out every widget it can lay out |
| /// except the [FloatingActionButton]. The [Scaffold] uses the [Offset] |
| /// returned from this method to position the [FloatingActionButton] and |
| /// complete its layout. |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry); |
| |
| @override |
| String toString() => objectRuntimeType(this, 'FloatingActionButtonLocation'); |
| } |
| |
| double _leftOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { |
| return kFloatingActionButtonMargin |
| + scaffoldGeometry.minInsets.left |
| - offset; |
| } |
| |
| double _rightOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { |
| return scaffoldGeometry.scaffoldSize.width |
| - kFloatingActionButtonMargin |
| - scaffoldGeometry.minInsets.right |
| - scaffoldGeometry.floatingActionButtonSize.width |
| + offset; |
| } |
| |
| double _endOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { |
| assert(scaffoldGeometry.textDirection != null); |
| switch (scaffoldGeometry.textDirection) { |
| case TextDirection.rtl: |
| return _leftOffset(scaffoldGeometry, offset: offset); |
| case TextDirection.ltr: |
| return _rightOffset(scaffoldGeometry, offset: offset); |
| } |
| return null; |
| } |
| |
| double _startOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { |
| assert(scaffoldGeometry.textDirection != null); |
| switch (scaffoldGeometry.textDirection) { |
| case TextDirection.rtl: |
| return _rightOffset(scaffoldGeometry, offset: offset); |
| case TextDirection.ltr: |
| return _leftOffset(scaffoldGeometry, offset: offset); |
| } |
| return null; |
| } |
| |
| class _CenterFloatFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _CenterFloatFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| // Compute the x-axis offset. |
| final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0; |
| |
| // Compute the y-axis offset. |
| final double contentBottom = scaffoldGeometry.contentBottom; |
| final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height; |
| final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; |
| final double snackBarHeight = scaffoldGeometry.snackBarSize.height; |
| double fabY = contentBottom - fabHeight - kFloatingActionButtonMargin; |
| if (snackBarHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin); |
| if (bottomSheetHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0); |
| |
| return Offset(fabX, fabY); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.centerFloat'; |
| } |
| |
| class _EndFloatFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _EndFloatFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| // Compute the x-axis offset. |
| final double fabX = _endOffset(scaffoldGeometry); |
| |
| // Compute the y-axis offset. |
| final double contentBottom = scaffoldGeometry.contentBottom; |
| final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height; |
| final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; |
| final double snackBarHeight = scaffoldGeometry.snackBarSize.height; |
| |
| double fabY = contentBottom - fabHeight - kFloatingActionButtonMargin; |
| if (snackBarHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin); |
| if (bottomSheetHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0); |
| |
| return Offset(fabX, fabY); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.endFloat'; |
| } |
| |
| // Provider of common logic for [FloatingActionButtonLocation]s that |
| // dock to the [BottomAppBar]. |
| abstract class _DockedFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _DockedFloatingActionButtonLocation(); |
| |
| // Positions the Y coordinate of the [FloatingActionButton] at a height |
| // where it docks to the [BottomAppBar]. |
| @protected |
| double getDockedY(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| final double contentBottom = scaffoldGeometry.contentBottom; |
| final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height; |
| final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; |
| final double snackBarHeight = scaffoldGeometry.snackBarSize.height; |
| |
| double fabY = contentBottom - fabHeight / 2.0; |
| // The FAB should sit with a margin between it and the snack bar. |
| if (snackBarHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin); |
| // The FAB should sit with its center in front of the top of the bottom sheet. |
| if (bottomSheetHeight > 0.0) |
| fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0); |
| |
| final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight; |
| return math.min(maxFabY, fabY); |
| } |
| } |
| |
| class _EndDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation { |
| const _EndDockedFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| final double fabX = _endOffset(scaffoldGeometry); |
| return Offset(fabX, getDockedY(scaffoldGeometry)); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.endDocked'; |
| } |
| |
| class _CenterDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation { |
| const _CenterDockedFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0; |
| return Offset(fabX, getDockedY(scaffoldGeometry)); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.centerDocked'; |
| } |
| |
| double _straddleAppBar(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0; |
| return scaffoldGeometry.contentTop - fabHalfHeight; |
| } |
| |
| class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _StartTopFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| return Offset(_startOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry)); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.startTop'; |
| } |
| |
| class _MiniStartTopFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _MiniStartTopFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| // We have to offset the FAB by four pixels because the FAB itself _adds_ |
| // four pixels in every direction in order to have a hit target area of 48 |
| // pixels in each dimension, despite being a circle of radius 40. |
| return Offset(_startOffset(scaffoldGeometry, offset: 4.0), _straddleAppBar(scaffoldGeometry)); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.miniStartTop'; |
| } |
| |
| class _EndTopFloatingActionButtonLocation extends FloatingActionButtonLocation { |
| const _EndTopFloatingActionButtonLocation(); |
| |
| @override |
| Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { |
| return Offset(_endOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry)); |
| } |
| |
| @override |
| String toString() => 'FloatingActionButtonLocation.endTop'; |
| } |
| |
| /// Provider of animations to move the [FloatingActionButton] between [FloatingActionButtonLocation]s. |
| /// |
| /// The [Scaffold] uses [Scaffold.floatingActionButtonAnimator] to define: |
| /// |
| /// * The [Offset] of the [FloatingActionButton] between the old and new |
| /// [FloatingActionButtonLocation]s as part of the transition animation. |
| /// * An [Animation] to scale the [FloatingActionButton] during the transition. |
| /// * An [Animation] to rotate the [FloatingActionButton] during the transition. |
| /// * Where to start a new animation from if an animation is interrupted. |
| /// |
| /// See also: |
| /// |
| /// * [FloatingActionButton], which is a circular button typically shown in the |
| /// bottom right corner of the app. |
| /// * [FloatingActionButtonLocation], which the [Scaffold] uses to place the |
| /// [Scaffold.floatingActionButton] within the [Scaffold]'s layout. |
| abstract class FloatingActionButtonAnimator { |
| /// Abstract const constructor. This constructor enables subclasses to provide |
| /// const constructors so that they can be used in const expressions. |
| const FloatingActionButtonAnimator(); |
| |
| /// Moves the [FloatingActionButton] by scaling out and then in at a new |
| /// [FloatingActionButtonLocation]. |
| /// |
| /// This animator shrinks the [FloatingActionButton] down until it disappears, then |
| /// grows it back to full size at its new [FloatingActionButtonLocation]. |
| /// |
| /// This is the default [FloatingActionButton] motion animation. |
| static const FloatingActionButtonAnimator scaling = _ScalingFabMotionAnimator(); |
| |
| /// Gets the [FloatingActionButton]'s position relative to the origin of the |
| /// [Scaffold] based on [progress]. |
| /// |
| /// [begin] is the [Offset] provided by the previous |
| /// [FloatingActionButtonLocation]. |
| /// |
| /// [end] is the [Offset] provided by the new |
| /// [FloatingActionButtonLocation]. |
| /// |
| /// [progress] is the current progress of the transition animation. |
| /// When [progress] is 0.0, the returned [Offset] should be equal to [begin]. |
| /// when [progress] is 1.0, the returned [Offset] should be equal to [end]. |
| Offset getOffset({ @required Offset begin, @required Offset end, @required double progress }); |
| |
| /// Animates the scale of the [FloatingActionButton]. |
| /// |
| /// The animation should both start and end with a value of 1.0. |
| /// |
| /// For example, to create an animation that linearly scales out and then back in, |
| /// you could join animations that pass each other: |
| /// |
| /// ```dart |
| /// @override |
| /// Animation<double> getScaleAnimation({@required Animation<double> parent}) { |
| /// // The animations will cross at value 0, and the train will return to 1.0. |
| /// return TrainHoppingAnimation( |
| /// Tween<double>(begin: 1.0, end: -1.0).animate(parent), |
| /// Tween<double>(begin: -1.0, end: 1.0).animate(parent), |
| /// ); |
| /// } |
| /// ``` |
| Animation<double> getScaleAnimation({ @required Animation<double> parent }); |
| |
| /// Animates the rotation of [Scaffold.floatingActionButton]. |
| /// |
| /// The animation should both start and end with a value of 0.0 or 1.0. |
| /// |
| /// The animation values are a fraction of a full circle, with 0.0 and 1.0 |
| /// corresponding to 0 and 360 degrees, while 0.5 corresponds to 180 degrees. |
| /// |
| /// For example, to create a rotation animation that rotates the |
| /// [FloatingActionButton] through a full circle: |
| /// |
| /// ```dart |
| /// @override |
| /// Animation<double> getRotationAnimation({@required Animation<double> parent}) { |
| /// return Tween<double>(begin: 0.0, end: 1.0).animate(parent); |
| /// } |
| /// ``` |
| Animation<double> getRotationAnimation({ @required Animation<double> parent }); |
| |
| /// Gets the progress value to restart a motion animation from when the animation is interrupted. |
| /// |
| /// [previousValue] is the value of the animation before it was interrupted. |
| /// |
| /// The restart of the animation will affect all three parts of the motion animation: |
| /// offset animation, scale animation, and rotation animation. |
| /// |
| /// An interruption triggers if the [Scaffold] is given a new [FloatingActionButtonLocation] |
| /// while it is still animating a transition between two previous [FloatingActionButtonLocation]s. |
| /// |
| /// A sensible default is usually 0.0, which is the same as restarting |
| /// the animation from the beginning, regardless of the original state of the animation. |
| double getAnimationRestart(double previousValue) => 0.0; |
| |
| @override |
| String toString() => objectRuntimeType(this, 'FloatingActionButtonAnimator'); |
| } |
| |
| class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator { |
| const _ScalingFabMotionAnimator(); |
| |
| @override |
| Offset getOffset({ Offset begin, Offset end, double progress }) { |
| if (progress < 0.5) { |
| return begin; |
| } else { |
| return end; |
| } |
| } |
| |
| @override |
| Animation<double> getScaleAnimation({ Animation<double> parent }) { |
| // Animate the scale down from 1 to 0 in the first half of the animation |
| // then from 0 back to 1 in the second half. |
| const Curve curve = Interval(0.5, 1.0, curve: Curves.ease); |
| return _AnimationSwap<double>( |
| ReverseAnimation(parent.drive(CurveTween(curve: curve.flipped))), |
| parent.drive(CurveTween(curve: curve)), |
| parent, |
| 0.5, |
| ); |
| } |
| |
| // Because we only see the last half of the rotation tween, |
| // it needs to go twice as far. |
| static final Animatable<double> _rotationTween = Tween<double>( |
| begin: 1.0 - kFloatingActionButtonTurnInterval * 2.0, |
| end: 1.0, |
| ); |
| |
| static final Animatable<double> _thresholdCenterTween = CurveTween(curve: const Threshold(0.5)); |
| |
| @override |
| Animation<double> getRotationAnimation({ Animation<double> parent }) { |
| // This rotation will turn on the way in, but not on the way out. |
| return _AnimationSwap<double>( |
| parent.drive(_rotationTween), |
| ReverseAnimation(parent.drive(_thresholdCenterTween)), |
| parent, |
| 0.5, |
| ); |
| } |
| |
| // If the animation was just starting, we'll continue from where we left off. |
| // If the animation was finishing, we'll treat it as if we were starting at that point in reverse. |
| // This avoids a size jump during the animation. |
| @override |
| double getAnimationRestart(double previousValue) => math.min(1.0 - previousValue, previousValue); |
| } |
| |
| /// An animation that swaps from one animation to the next when the [parent] passes [swapThreshold]. |
| /// |
| /// The [value] of this animation is the value of [first] when [parent.value] < [swapThreshold] |
| /// and the value of [next] otherwise. |
| class _AnimationSwap<T> extends CompoundAnimation<T> { |
| /// Creates an [_AnimationSwap]. |
| /// |
| /// Both arguments must be non-null. Either can be an [_AnimationSwap] itself |
| /// to combine multiple animations. |
| _AnimationSwap(Animation<T> first, Animation<T> next, this.parent, this.swapThreshold) : super(first: first, next: next); |
| |
| final Animation<double> parent; |
| final double swapThreshold; |
| |
| @override |
| T get value => parent.value < swapThreshold ? first.value : next.value; |
| } |