blob: f8e1d78e9a01d3a44a57a3c080c4d631860392da [file] [log] [blame]
// 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();
/// 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 --template=stateless_widget_material_no_null_safety}
///
/// 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].
///
/// ```dart preamble
/// class AlmostEndFloatFabLocation extends StandardFabLocation
/// with FabEndOffsetX, FabFloatOffsetY {
/// @override
/// double getOffsetX (ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) {
/// final double directionalAdjustment =
/// scaffoldGeometry.textDirection == TextDirection.ltr ? -50.0 : 50.0;
/// return super.getOffsetX(scaffoldGeometry, adjustment) + directionalAdjustment;
/// }
/// }
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Home page'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () { print('FAB pressed.'); },
/// tooltip: 'Increment',
/// child: Icon(Icons.add),
/// ),
/// floatingActionButtonLocation: AlmostEndFloatFabLocation(),
/// );
/// }
/// ```
/// {@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,
);
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 safeMargin = bottomViewPadding > contentMargin ? bottomViewPadding : 0.0;
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 "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';
}
/// 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;
}