// 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'; /// 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; /// If a [FloatingActionButton] is used on a [Scaffold] in certain positions, /// it is moved [kMiniButtonOffsetAdjustment] pixels closer to the edge of the screen. /// /// 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]. /// /// More specifically: /// * In the following positions, the [FloatingActionButton] is moved *horizontally* /// closer to the edge of the screen: /// * [FloatingActionButtonLocation.miniStartTop] /// * [FloatingActionButtonLocation.miniStartFloat] /// * [FloatingActionButtonLocation.miniStartDocked] /// * [FloatingActionButtonLocation.miniEndTop] /// * [FloatingActionButtonLocation.miniEndFloat] /// * [FloatingActionButtonLocation.miniEndDocked] /// * In the following positions, the [FloatingActionButton] is moved *vertically* /// closer to the bottom of the screen: /// * [FloatingActionButtonLocation.miniStartFloat] /// * [FloatingActionButtonLocation.miniCenterFloat] /// * [FloatingActionButtonLocation.miniEndFloat] const double kMiniButtonOffsetAdjustment = 4.0; /// 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. /// /// ## Floating Action Button placements /// /// The following diagrams show the available placement locations for the FloatingActionButton. /// /// * [FloatingActionButtonLocation.centerDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_docked.png) /// /// /// * [FloatingActionButtonLocation.centerFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_float.png) /// /// /// * [FloatingActionButtonLocation.centerTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_top.png) /// /// /// * [FloatingActionButtonLocation.endDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_docked.png) /// /// /// * [FloatingActionButtonLocation.endFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_float.png) /// /// /// * [FloatingActionButtonLocation.endTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_top.png) /// /// /// * [FloatingActionButtonLocation.startDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_docked.png) /// /// /// * [FloatingActionButtonLocation.startFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_float.png) /// /// /// * [FloatingActionButtonLocation.startTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_top.png) /// /// /// * [FloatingActionButtonLocation.miniCenterDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_docked.png) /// /// /// * [FloatingActionButtonLocation.miniCenterFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_float.png) /// /// /// * [FloatingActionButtonLocation.miniCenterTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_top.png) /// /// /// * [FloatingActionButtonLocation.miniEndDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_docked.png) /// /// /// * [FloatingActionButtonLocation.miniEndFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_float.png) /// /// /// * [FloatingActionButtonLocation.miniEndTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_top.png) /// /// /// * [FloatingActionButtonLocation.miniStartDocked]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_docked.png) /// /// /// * [FloatingActionButtonLocation.miniStartFloat]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_float.png) /// /// /// * [FloatingActionButtonLocation.miniStartTop]: /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_top.png) /// /// /// 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(); /// Start-aligned [FloatingActionButton], floating over the transition between /// the [Scaffold.appBar] and the [Scaffold.body]. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.leading] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniStartTop] and set [FloatingActionButton.mini] to true. /// /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_top.png) static const FloatingActionButtonLocation startTop = _StartTopFabLocation(); /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_top.png) static const FloatingActionButtonLocation miniStartTop = _MiniStartTopFabLocation(); /// Centered [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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_top.png) static const FloatingActionButtonLocation centerTop = _CenterTopFabLocation(); /// Centered [FloatingActionButton], floating over the transition between /// the [Scaffold.appBar] and the [Scaffold.body], intended to be used with /// [FloatingActionButton.mini] set to true. /// /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_top.png) static const FloatingActionButtonLocation miniCenterTop = _MiniCenterTopFabLocation(); /// End-aligned [FloatingActionButton], floating over the transition between /// the [Scaffold.appBar] and the [Scaffold.body]. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.trailing] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniEndTop] and set [FloatingActionButton.mini] to true. /// /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_top.png) static const FloatingActionButtonLocation endTop = _EndTopFabLocation(); /// End-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.trailing] 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_top.png) static const FloatingActionButtonLocation miniEndTop = _MiniEndTopFabLocation(); /// Start-aligned [FloatingActionButton], floating at the bottom of the screen. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.leading] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniStartFloat] and set [FloatingActionButton.mini] to true. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_float.png) static const FloatingActionButtonLocation startFloat = _StartFloatFabLocation(); /// Start-aligned [FloatingActionButton], floating at the bottom of the screen, /// 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]. /// /// Compared to [FloatingActionButtonLocation.startFloat], floating action /// buttons using this location will move horizontally _and_ vertically /// closer to the edges, by [kMiniButtonOffsetAdjustment] each. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_float.png) static const FloatingActionButtonLocation miniStartFloat = _MiniStartFloatFabLocation(); /// Centered [FloatingActionButton], floating at the bottom of the screen. /// /// To position a mini floating action button, use [miniCenterFloat] and /// set [FloatingActionButton.mini] to true. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_float.png) static const FloatingActionButtonLocation centerFloat = _CenterFloatFabLocation(); /// Centered [FloatingActionButton], floating at the bottom of the screen, /// 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 horizontally with /// the locations [FloatingActionButtonLocation.miniStartFloat] /// and [FloatingActionButtonLocation.miniEndFloat]. /// /// Compared to [FloatingActionButtonLocation.centerFloat], floating action /// buttons using this location will move vertically down /// by [kMiniButtonOffsetAdjustment]. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_float.png) static const FloatingActionButtonLocation miniCenterFloat = _MiniCenterFloatFabLocation(); /// End-aligned [FloatingActionButton], floating at the bottom of the screen. /// /// This is the default alignment of [FloatingActionButton]s in Material applications. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.trailing] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniEndFloat] and set [FloatingActionButton.mini] to true. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_float.png) static const FloatingActionButtonLocation endFloat = _EndFloatFabLocation(); /// End-aligned [FloatingActionButton], floating at the bottom of the screen, /// 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.trailing] slot of a [ListTile] in a [ListView] in the /// [Scaffold.body]. /// /// Compared to [FloatingActionButtonLocation.endFloat], floating action /// buttons using this location will move horizontally _and_ vertically /// closer to the edges, by [kMiniButtonOffsetAdjustment] each. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_float.png) static const FloatingActionButtonLocation miniEndFloat = _MiniEndFloatFabLocation(); /// Start-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. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.leading] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniStartDocked] and set [FloatingActionButton.mini] to true. /// /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_start_docked.png) static const FloatingActionButtonLocation startDocked = _StartDockedFabLocation(); /// Start-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, /// optimized for mini floating action buttons. /// /// 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 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 bottom /// navigation bar. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_start_docked.png) static const FloatingActionButtonLocation miniStartDocked = _MiniStartDockedFabLocation(); /// Centered [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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_center_docked.png) static const FloatingActionButtonLocation centerDocked = _CenterDockedFabLocation(); /// Centered [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; /// intended to be used with [FloatingActionButton.mini] set to true. /// /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_center_docked.png) static const FloatingActionButtonLocation miniCenterDocked = _MiniCenterDockedFabLocation(); /// 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. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_docked.png) static const FloatingActionButtonLocation endDocked = _EndDockedFabLocation(); /// 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, /// optimized for mini floating action buttons. /// /// To align a floating action button with [CircleAvatar]s in the /// [ListTile.trailing] slots of [ListTile]s in a [ListView] in the [Scaffold.body], /// use [miniEndDocked] and set [FloatingActionButton.mini] to true. /// /// 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 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.trailing] slot of a [ListTile] in a [ListView] in the /// [Scaffold.body]. /// /// This is unlikely to be a useful location for apps that lack a bottom /// navigation bar. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_docked.png) static const FloatingActionButtonLocation miniEndDocked = _MiniEndDockedFabLocation(); /// End-aligned [FloatingActionButton], floating over the /// [Scaffold.bottomNavigationBar] so that the floating /// action button lines up with the center of the bottom navigation bar. /// /// This is unlikely to be a useful location for apps which has a [BottomNavigationBar] /// or a non material 3 [BottomAppBar]. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_contained.png) static const FloatingActionButtonLocation endContained = _EndContainedFabLocation(); /// 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'); } /// A base class that simplifies building [FloatingActionButtonLocation]s when /// used with mixins [FabTopOffsetY], [FabFloatOffsetY], [FabDockedOffsetY], /// [FabStartOffsetX], [FabCenterOffsetX], [FabEndOffsetX], and [FabMiniOffsetAdjustment]. /// /// A subclass of [FloatingActionButtonLocation] which implements its [getOffset] method /// using three other methods: [getOffsetX], [getOffsetY], and [isMini]. /// /// Different mixins on this class override different methods, so that combining /// a set of mixins creates a floating action button location. /// /// For example: the location [FloatingActionButtonLocation.miniEndTop] /// is based on a class that extends [StandardFabLocation] /// with mixins [FabMiniOffsetAdjustment], [FabEndOffsetX], and [FabTopOffsetY]. /// /// You can create your own subclass of [StandardFabLocation] /// to implement a custom [FloatingActionButtonLocation]. /// /// {@tool dartpad} /// This is an example of a user-defined [FloatingActionButtonLocation]. /// /// The example shows a [Scaffold] with an [AppBar], a [BottomAppBar], and a /// [FloatingActionButton] using a custom [FloatingActionButtonLocation]. /// /// The new [FloatingActionButtonLocation] is defined /// by extending [StandardFabLocation] with two mixins, /// [FabEndOffsetX] and [FabFloatOffsetY], and overriding the /// [getOffsetX] method to adjust the FAB's x-coordinate, creating a /// [FloatingActionButtonLocation] slightly different from /// [FloatingActionButtonLocation.endFloat]. /// /// ** See code in examples/api/lib/material/floating_action_button_location/standard_fab_location.0.dart ** /// {@end-tool} /// abstract class StandardFabLocation extends FloatingActionButtonLocation { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const StandardFabLocation(); /// Obtains the x-offset to place the [FloatingActionButton] based on the /// [Scaffold]'s layout. /// /// Used by [getOffset] to compute its x-coordinate. double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment); /// Obtains the y-offset to place the [FloatingActionButton] based on the /// [Scaffold]'s layout. /// /// Used by [getOffset] to compute its y-coordinate. double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment); /// A function returning whether this [StandardFabLocation] is optimized for /// mini [FloatingActionButton]s. bool isMini () => false; @override Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { final double adjustment = isMini() ? kMiniButtonOffsetAdjustment : 0.0; return Offset( getOffsetX(scaffoldGeometry, adjustment), getOffsetY(scaffoldGeometry, adjustment), ); } /// Calculates x-offset for left-aligned [FloatingActionButtonLocation]s. static double _leftOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { return kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left - adjustment; } /// Calculates x-offset for right-aligned [FloatingActionButtonLocation]s. static double _rightOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { return scaffoldGeometry.scaffoldSize.width - kFloatingActionButtonMargin - scaffoldGeometry.minInsets.right - scaffoldGeometry.floatingActionButtonSize.width + adjustment; } } /// Mixin for a "top" floating action button location, such as /// [FloatingActionButtonLocation.startTop]. /// /// The `adjustment`, typically [kMiniButtonOffsetAdjustment], is ignored in the /// Y axis of "top" positions. For "top" positions, the X offset is adjusted to /// move closer to the edge of the screen. This is so that a minified floating /// action button appears to align with [CircleAvatar]s in the /// [ListTile.leading] slot of a [ListTile] in a [ListView] in the /// [Scaffold.body]. mixin FabTopOffsetY on StandardFabLocation { /// Calculates y-offset for [FloatingActionButtonLocation]s floating over /// the transition between the [Scaffold.appBar] and the [Scaffold.body]. @override double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { if (scaffoldGeometry.contentTop > scaffoldGeometry.minViewPadding.top) { final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0; return scaffoldGeometry.contentTop - fabHalfHeight; } // Otherwise, ensure we are placed within the bounds of a safe area. return scaffoldGeometry.minViewPadding.top; } } /// Mixin for a "float" floating action button location, such as [FloatingActionButtonLocation.centerFloat]. mixin FabFloatOffsetY on StandardFabLocation { /// Calculates y-offset for [FloatingActionButtonLocation]s floating at /// the bottom of the screen. @override double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { final double contentBottom = scaffoldGeometry.contentBottom; final double bottomContentHeight = scaffoldGeometry.scaffoldSize.height - contentBottom; final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height; final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; final double snackBarHeight = scaffoldGeometry.snackBarSize.height; final double safeMargin = math.max( kFloatingActionButtonMargin, scaffoldGeometry.minViewPadding.bottom - bottomContentHeight + kFloatingActionButtonMargin, ); double fabY = contentBottom - fabHeight - safeMargin; 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 fabY + adjustment; } } /// Mixin for a "docked" floating action button location, such as [FloatingActionButtonLocation.endDocked]. mixin FabDockedOffsetY on StandardFabLocation { /// Calculates y-offset for [FloatingActionButtonLocation]s floating over the /// [Scaffold.bottomNavigationBar] so that the center of the floating /// action button lines up with the top of the bottom navigation bar. @override double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { final double contentBottom = scaffoldGeometry.contentBottom; final double contentMargin = scaffoldGeometry.scaffoldSize.height - contentBottom; final double bottomViewPadding = scaffoldGeometry.minViewPadding.bottom; final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height; final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; final double snackBarHeight = scaffoldGeometry.snackBarSize.height; final double bottomMinInset = scaffoldGeometry.minInsets.bottom; double safeMargin; if (contentMargin > bottomMinInset + fabHeight / 2.0) { // If contentMargin is higher than bottomMinInset enough to display the // FAB without clipping, don't provide a margin safeMargin = 0.0; } else if (bottomMinInset == 0.0) { // If bottomMinInset is zero(the software keyboard is not on the screen) // provide bottomViewPadding as margin safeMargin = bottomViewPadding; } else { // Provide a margin that would shift the FAB enough so that it stays away // from the keyboard safeMargin = fabHeight / 2.0 + kFloatingActionButtonMargin; } double fabY = contentBottom - fabHeight / 2.0 - safeMargin; // 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 - safeMargin; return math.min(maxFabY, fabY); } } /// Mixin for a "contained" floating action button location, such as [FloatingActionButtonLocation.endContained]. mixin FabContainedOffsetY on StandardFabLocation { /// Calculates y-offset for [FloatingActionButtonLocation]s floating over the /// [Scaffold.bottomNavigationBar] so that the center of the floating /// action button lines up with the center of the bottom navigation bar. @override double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { final double contentBottom = scaffoldGeometry.contentBottom; final double contentMargin = scaffoldGeometry.scaffoldSize.height - contentBottom; final double bottomViewPadding = scaffoldGeometry.minViewPadding.bottom; final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height; final double bottomMinInset = scaffoldGeometry.minInsets.bottom; double safeMargin = 0.0; if (contentMargin > bottomMinInset + fabHeight / 2.0) { // If contentMargin is higher than bottomMinInset enough to display the // FAB without clipping, don't provide a margin safeMargin = 0.0; } else { safeMargin = bottomViewPadding; } final double fabY = contentBottom - fabHeight / 2.0 - safeMargin; final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight - safeMargin; return math.min(maxFabY, fabY + contentMargin / 2); } } /// Mixin for a "start" floating action button location, such as [FloatingActionButtonLocation.startTop]. mixin FabStartOffsetX on StandardFabLocation { /// Calculates x-offset for start-aligned [FloatingActionButtonLocation]s. @override double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { assert(scaffoldGeometry.textDirection != null); switch (scaffoldGeometry.textDirection) { case TextDirection.rtl: return StandardFabLocation._rightOffsetX(scaffoldGeometry, adjustment); case TextDirection.ltr: return StandardFabLocation._leftOffsetX(scaffoldGeometry, adjustment); } } } /// Mixin for a "center" floating action button location, such as [FloatingActionButtonLocation.centerFloat]. mixin FabCenterOffsetX on StandardFabLocation { /// Calculates x-offset for center-aligned [FloatingActionButtonLocation]s. @override double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { return (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0; } } /// Mixin for an "end" floating action button location, such as [FloatingActionButtonLocation.endDocked]. mixin FabEndOffsetX on StandardFabLocation { /// Calculates x-offset for end-aligned [FloatingActionButtonLocation]s. @override double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { assert(scaffoldGeometry.textDirection != null); switch (scaffoldGeometry.textDirection) { case TextDirection.rtl: return StandardFabLocation._leftOffsetX(scaffoldGeometry, adjustment); case TextDirection.ltr: return StandardFabLocation._rightOffsetX(scaffoldGeometry, adjustment); } } } /// Mixin for a "mini" floating action button location, such as [FloatingActionButtonLocation.miniStartTop]. mixin FabMiniOffsetAdjustment on StandardFabLocation { @override bool isMini () => true; } class _StartTopFabLocation extends StandardFabLocation with FabStartOffsetX, FabTopOffsetY { const _StartTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.startTop'; } class _MiniStartTopFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabStartOffsetX, FabTopOffsetY { const _MiniStartTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniStartTop'; } class _CenterTopFabLocation extends StandardFabLocation with FabCenterOffsetX, FabTopOffsetY { const _CenterTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.centerTop'; } class _MiniCenterTopFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabCenterOffsetX, FabTopOffsetY { const _MiniCenterTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniCenterTop'; } class _EndTopFabLocation extends StandardFabLocation with FabEndOffsetX, FabTopOffsetY { const _EndTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.endTop'; } class _MiniEndTopFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabEndOffsetX, FabTopOffsetY { const _MiniEndTopFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniEndTop'; } class _StartFloatFabLocation extends StandardFabLocation with FabStartOffsetX, FabFloatOffsetY { const _StartFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.startFloat'; } class _MiniStartFloatFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabStartOffsetX, FabFloatOffsetY { const _MiniStartFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniStartFloat'; } class _CenterFloatFabLocation extends StandardFabLocation with FabCenterOffsetX, FabFloatOffsetY { const _CenterFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.centerFloat'; } class _MiniCenterFloatFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabCenterOffsetX, FabFloatOffsetY { const _MiniCenterFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniCenterFloat'; } class _EndFloatFabLocation extends StandardFabLocation with FabEndOffsetX, FabFloatOffsetY { const _EndFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.endFloat'; } class _MiniEndFloatFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabEndOffsetX, FabFloatOffsetY { const _MiniEndFloatFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniEndFloat'; } class _StartDockedFabLocation extends StandardFabLocation with FabStartOffsetX, FabDockedOffsetY { const _StartDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.startDocked'; } class _MiniStartDockedFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabStartOffsetX, FabDockedOffsetY { const _MiniStartDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniStartDocked'; } class _CenterDockedFabLocation extends StandardFabLocation with FabCenterOffsetX, FabDockedOffsetY { const _CenterDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.centerDocked'; } class _MiniCenterDockedFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabCenterOffsetX, FabDockedOffsetY { const _MiniCenterDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniCenterDocked'; } class _EndDockedFabLocation extends StandardFabLocation with FabEndOffsetX, FabDockedOffsetY { const _EndDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.endDocked'; } class _MiniEndDockedFabLocation extends StandardFabLocation with FabMiniOffsetAdjustment, FabEndOffsetX, FabDockedOffsetY { const _MiniEndDockedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.miniEndDocked'; } class _EndContainedFabLocation extends StandardFabLocation with FabEndOffsetX, FabContainedOffsetY { const _EndContainedFabLocation(); @override String toString() => 'FloatingActionButtonLocation.endContained'; } /// 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({ required Offset begin, required Offset end, required double progress }) { if (progress < 0.5) { return begin; } else { return end; } } @override Animation<double> getScaleAnimation({ required 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({ required 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; }