Unverified Commit dd0acea1 authored by David Shuckerow's avatar David Shuckerow Committed by GitHub

Add support for placing the FAB in different positions (#14368)

* Add support to move the fab between positions.

* Motion demo for the FAB works between center and end floating.

* Add a Material curve to the offset animation.

* Move the fab position into an object

* Updates to docs

* Updates to docs

* Fix a lint on the bottom sheet type

* Add a ScaffoldGeometry class

* Improve the documentation

* Improve the documentation

* Add a fab motion animator

* Add position and scale animations

* FAB entrance and motion animations work

* Get started on FAB motion

* Make fab animation work properly.

* Change the fab animator to be stored in the state of the scaffold.

* Add a layout test

* Fix spacing being off

* Fix the entrance/exit animation test.

* Add a textDirection to the layout delegate.

* Fix const constructor lint checks

* Add toStrings for the fab positioner/animator

* Add a toString for CurveTween

* Change the fab motion demo icon to a simple add icon.

* Add tests and a custom fab positioner to the demo.

* Do not start the fab's motion animation when the fab is null.

* Adjust the code to pass the new tests.

* Rename for in response to Hans' comment.

* Revert the tabs fab demo

* Use timeDilation, and clean up the animation code a little.

* Clean up the prelayout geometry docs and ctr order

* Cleanup fab transition widget code

* Clean up comments on Scaffold, add cross-references between the two geometries

* Explain the fab motion animation scheduling better

* Add a const to the fab motion demo

* Make the fab animation never jank by keeping track of where to move the fab to in the future.

* Add a default fab positioner constant

* Add space after comma in the demo

* Add boilerplate dartdoc to all const constructors

* Comment improvement

* Rename 'fabSize' to 'floatingActionButtonSize'

* Rename 'fabSize' to 'floatingActionButtonSize'

* Rename 'fabSize' to 'floatingActionButtonSize'

* Clean up the prelayout geometry object's dartdoc

* Clean up the prelayout geometry object's dartdoc

* Remove extraneous comment

* Change possessive uses of Scaffold's to use dartdoc-compatible [Scaffold]'s

* Rename the horizontalFabPadding to an expansion

* Clean up controller cleanup and setState usage

* Animate instead of lerp

* Make the fab position animation use offsets instead of animations

* Streamline the fab motion demo

* Set up the animator to start from a reasonable place when interrupting animations.

* Doc cleanup on the new animation interruption

* Expand some uses of fab and clean up constants

* Expand remaining public uses of fab to floating action button

* Expand remaining public uses of fab to floating action button

* Expand on the documentation for the fab positioner and animator

* Refactor animations to broadcast the position properly.

* Add the ability to turn on and off the fab to the motion demo.

* Remove unused code

* Change the fab animator to animate even when the fab is exitting.

* Remove extra positioner.

* Apps -> Applications in docs

* Explain the scale animation.

* Name the child parameter in the animated builder

* RTL before LTR

* Wrap the AppBar in the example code

* const the fab motion demo name

* Start a test against animation jumps

* Test for jumps in the fab motion animation

* Dont initialize values to null

* Use constants, fix spacing from some of Hans' comments

* Clarify the relationship between fab positioners and prelayout geometries

* Explain the fab animmator a bit better

* Explain the animation progress in the fab animation

* Explain the animation restart better

* Explain the animation restart better

* Explain the prelayout geometry better

* Explain that height is a vertical distance.

* Explain the horizontal fab padding

* Update the scaffold size description to explain what happens when a wild keyboard appears

* Remove print statements

* Update the scaffold geometry with information about it being available at paint time.

* In one step of a transition

* Explain how the top-start fab positioner works

* Explain how the top-start fab positioner works

* Refactor the scaffold layout to just pass a padding instead of a bottom, top, start and end.

* Refactor the scaffold layout to just pass a padding instead of a bottom, top, start and end.

* Action buttons with with custom positioners.

* Add a rotation animation example.

* Use a swap animation to show swapping between two different animations.

* Use a swap animation to show swapping between two different animations.

* Add an example for the size animations.

* 2018 copyright

* Extra empty line

* Return new Scaffold

* Extra blank line fix

* All its contents have been laid out

* Position the fab

* Explain what the scaffold geometry is for.

* Move asserts to different lines

* The scaffoldsize will not

* Initial rename of FabPositioners to FloatingActionButtonLocation

* Rename comments in example to refer to location instead of positioner.

* Rename fabpositioner to location in tests and in the scaffold field

* Finish removing references to positioner in scaffold code.

* Split the fab location and animation out into a separate file.

* Make things more private

* Import foundation instead of meta

* Const curve instead of final.
parent 600739d1
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
const String _explanatoryText =
"When the Scaffold's floating action button location changes, "
'the floating action button animates to its new position';
class FabMotionDemo extends StatefulWidget {
static const String routeName = '/material/fab-motion';
@override
_FabMotionDemoState createState() {
return new _FabMotionDemoState();
}
}
class _FabMotionDemoState extends State<FabMotionDemo> {
static const List<FloatingActionButtonLocation> _floatingActionButtonLocations = const <FloatingActionButtonLocation>[
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.centerFloat,
const _TopStartFloatingActionButtonLocation(),
];
bool _showFab = true;
FloatingActionButtonLocation _floatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
@override
Widget build(BuildContext context) {
final Widget floatingActionButton = _showFab
? new Builder(builder: (BuildContext context) {
// We use a widget builder here so that this inner context can find the Scaffold.
// This makes it possible to show the snackbar.
return new FloatingActionButton(
backgroundColor: Colors.yellow.shade900,
onPressed: () => _showSnackbar(context),
child: const Icon(Icons.add),
);
})
: null;
return new Scaffold(
appBar: new AppBar(
title: const Text('FAB Location'),
// Add 48dp of space onto the bottom of the appbar.
// This gives space for the top-start location to attach to without
// blocking the 'back' button.
bottom: const PreferredSize(
preferredSize: const Size.fromHeight(48.0),
child: const SizedBox(),
),
),
floatingActionButtonLocation: _floatingActionButtonLocation,
floatingActionButton: floatingActionButton,
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new RaisedButton(
onPressed: _moveFab,
child: const Text('MOVE FAB'),
),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Toggle FAB'),
new Switch(value: _showFab, onChanged: _toggleFab),
],
),
],
),
),
);
}
void _moveFab() {
setState(() {
_floatingActionButtonLocation = _floatingActionButtonLocations[(_floatingActionButtonLocations.indexOf(_floatingActionButtonLocation) + 1) % _floatingActionButtonLocations.length];
});
}
void _toggleFab(bool showFab) {
setState(() {
_showFab = showFab;
});
}
void _showSnackbar(BuildContext context) {
Scaffold.of(context).showSnackBar(const SnackBar(content: const Text(_explanatoryText)));
}
}
// Places the Floating Action Button at the top of the content area of the
// app, on the border between the body and the app bar.
class _TopStartFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _TopStartFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
// First, we'll place the X coordinate for the Floating Action Button
// at the start of the screen, based on the text direction.
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
// In RTL layouts, the start of the screen is on the right side,
// and the end of the screen is on the left.
//
// We need to align the right edge of the floating action button with
// the right edge of the screen, then move it inwards by the designated padding.
//
// The Scaffold's origin is at its top-left, so we need to offset fabX
// by the Scaffold's width to get the right edge of the screen.
//
// The Floating Action Button's origin is at its top-left, so we also need
// to subtract the Floating Action Button's width to align the right edge
// of the Floating Action Button instead of the left edge.
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - startPadding;
break;
case TextDirection.ltr:
// In LTR layouts, the start of the screen is on the left side,
// and the end of the screen is on the right.
//
// Placing the fabX at 0.0 will align the left edge of the
// Floating Action Button with the left edge of the screen, so all
// we need to do is offset fabX by the designated padding.
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left;
fabX = startPadding;
break;
}
// Finally, we'll place the Y coordinate for the Floating Action Button
// at the top of the content body.
//
// We want to place the middle of the Floating Action Button on the
// border between the Scaffold's app bar and its body. To do this,
// we place fabY at the scaffold geometry's contentTop, then subtract
// half of the Floating Action Button's height to place the center
// over the contentTop.
//
// We don't have to worry about which way is the top like we did
// for left and right, so we place fabY in this one-liner.
final double fabY = scaffoldGeometry.contentTop - (scaffoldGeometry.floatingActionButtonSize.height / 2.0);
return new Offset(fabX, fabY);
}
}
......@@ -11,6 +11,7 @@ export 'date_and_time_picker_demo.dart';
export 'dialog_demo.dart';
export 'drawer_demo.dart';
export 'expansion_panels_demo.dart';
export 'fab_motion_demo.dart';
export 'grid_list_demo.dart';
export 'icons_demo.dart';
export 'leave_behind_demo.dart';
......
......@@ -158,6 +158,13 @@ List<GalleryItem> _buildGalleryItems() {
routeName: TabsFabDemo.routeName,
buildRoute: (BuildContext context) => new TabsFabDemo(),
),
new GalleryItem(
title: 'Floating action button motion',
subtitle: 'Action buttons with customized positions',
category: 'Material Components',
routeName: FabMotionDemo.routeName,
buildRoute: (BuildContext context) => new FabMotionDemo(),
),
new GalleryItem(
title: 'Grid',
subtitle: 'Row and column layout',
......
......@@ -49,6 +49,7 @@ export 'src/material/feedback.dart';
export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart';
export 'src/material/floating_action_button.dart';
export 'src/material/floating_action_button_location.dart';
export 'src/material/flutter_logo.dart';
export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart';
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart: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 = const 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 = const _EndFloatFabLocation();
/// Centered [FloatingActionButton], floating at the bottom of the screen.
static const FloatingActionButtonLocation centerFloat = const _CenterFloatFabLocation();
/// 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() => '$runtimeType';
}
class _CenterFloatFabLocation extends FloatingActionButtonLocation {
const _CenterFloatFabLocation();
@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 new Offset(fabX, fabY);
}
}
class _EndFloatFabLocation extends FloatingActionButtonLocation {
const _EndFloatFabLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
// Compute the x-axis offset.
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
// In RTL, the end of the screen is the left.
final double endPadding = scaffoldGeometry.minInsets.left;
fabX = kFloatingActionButtonMargin + endPadding;
break;
case TextDirection.ltr:
// In LTR, the end of the screen is the right.
final double endPadding = scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - kFloatingActionButtonMargin - endPadding;
break;
}
// 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 new Offset(fabX, fabY);
}
}
/// 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 = const _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 new 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 new 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() => '$runtimeType';
}
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 = const Interval(0.5, 1.0, curve: Curves.ease);
return new _AnimationSwap<double>(
new ReverseAnimation(new CurveTween(curve: curve.flipped).animate(parent)),
new CurveTween(curve: curve).animate(parent),
parent,
0.5,
);
}
@override
Animation<double> getRotationAnimation({Animation<double> parent}) {
// Because we only see the last half of the rotation tween,
// it needs to go twice as far.
final Tween<double> rotationTween = new Tween<double>(
begin: 1.0 - kFloatingActionButtonTurnInterval * 2,
end: 1.0,
);
// This rotation will turn on the way in, but not on the way out.
return new _AnimationSwap<double>(
rotationTween.animate(parent),
new ReverseAnimation(new CurveTween(curve: const Threshold(0.5)).animate(parent)),
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 [AnimationMin] 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;
}
\ No newline at end of file
......@@ -8,6 +8,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'app_bar.dart';
......@@ -17,13 +18,13 @@ import 'button_theme.dart';
import 'divider.dart';
import 'drawer.dart';
import 'flexible_space_bar.dart';
import 'floating_action_button_location.dart';
import 'material.dart';
import 'snack_bar.dart';
import 'theme.dart';
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0);
const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling;
/// Returns a path for a notch in the outline of a shape.
///
......@@ -56,10 +57,145 @@ enum _ScaffoldSlot {
statusBar,
}
/// Geometry information for [Scaffold] components.
/// The geometry of the [Scaffold] after all its contents have been laid out
/// except the [FloatingActionButton].
///
/// The [Scaffold] passes this prelayout geometry to its
/// [FloatingActionButtonLocation], which produces an [Offset] that the
/// [Scaffold] uses to position the [FloatingActionButton].
///
/// For a description of the [Scaffold]'s geometry after it has
/// finished laying out, see the [ScaffoldGeometry].
@immutable
class ScaffoldPrelayoutGeometry {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ScaffoldPrelayoutGeometry({
@required this.bottomSheetSize,
@required this.contentBottom,
@required this.contentTop,
@required this.floatingActionButtonSize,
@required this.minInsets,
@required this.scaffoldSize,
@required this.snackBarSize,
@required this.textDirection,
});
/// The [Size] of [Scaffold.floatingActionButton].
///
/// If [Scaffold.floatingActionButton] is null, this will be [Size.zero].
final Size floatingActionButtonSize;
/// The [Size] of the [Scaffold]'s [BottomSheet].
///
/// If the [Scaffold] is not currently showing a [BottomSheet],
/// this will be [Size.zero].
final Size bottomSheetSize;
/// The vertical distance from the Scaffold's origin to the bottom of
/// [Scaffold.body].
///
/// This is useful in a [FloatingActionButtonLocation] designed to
/// place the [FloatingActionButton] at the bottom of the screen, while
/// keeping it above the [BottomSheet], the [Scaffold.bottomNavigationBar],
/// or the keyboard.
///
/// Note that [Scaffold.body] is laid out with respect to [minInsets] already.
/// This means that a [FloatingActionButtonLocation] does not need to factor
/// in [minInsets.bottom] when aligning a [FloatingActionButton] to [contentBottom].
final double contentBottom;
/// The vertical distance from the [Scaffold]'s origin to the top of
/// [Scaffold.body].
///
/// This is useful in a [FloatingActionButtonLocation] designed to
/// place the [FloatingActionButton] at the top of the screen, while
/// keeping it below the [Scaffold.appBar].
///
/// Note that [Scaffold.body] is laid out with respect to [minInsets] already.
/// This means that a [FloatingActionButtonLocation] does not need to factor
/// in [minInsets.top] when aligning a [FloatingActionButton] to [contentTop].
final double contentTop;
/// The minimum padding to inset the [FloatingActionButton] by for it
/// to remain visible.
///
/// This value is the result of calling [MediaQuery.padding] in the
/// [Scaffold]'s [BuildContext],
/// and is useful for insetting the [FloatingActionButton] to avoid features like
/// the system status bar or the keyboard.
///
/// If [Scaffold.resizeToAvoidBottomPadding] is set to false, [minInsets.bottom]
/// will be 0.0 instead of [MediaQuery.padding.bottom].
final EdgeInsets minInsets;
/// The [Size] of the whole [Scaffold].
///
/// If the [Size] of the [Scaffold]'s contents is modified by values such as
/// [Scaffold.resizeToAvoidBottomPadding] or the keyboard opening, then the
/// [scaffoldSize] will not reflect those changes.
///
/// This means that [FloatingActionButtonLocation]s designed to reposition
/// the [FloatingActionButton] based on events such as the keyboard popping
/// up should use [minInsets] to make sure that the [FloatingActionButton] is
/// inset by enough to remain visible.
///
/// See [minInsets] and [MediaQuery.padding] for more information on the appropriate
/// insets to apply.
final Size scaffoldSize;
/// The [Size] of the [Scaffold]'s [SnackBar].
///
/// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero].
final Size snackBarSize;
/// The [TextDirection] of the [Scaffold]'s [BuildContext].
final TextDirection textDirection;
}
/// A snapshot of a transition between two [FloatingActionButtonLocation]s.
///
/// [ScaffoldState] uses this to seamlessly change transition animations
/// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition.
@immutable
class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation {
const _TransitionSnapshotFabLocation(this.begin, this.end, this.animator, this.progress);
final FloatingActionButtonLocation begin;
final FloatingActionButtonLocation end;
final FloatingActionButtonAnimator animator;
final double progress;
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
return animator.getOffset(
begin: begin.getOffset(scaffoldGeometry),
end: end.getOffset(scaffoldGeometry),
progress: progress,
);
}
@override
String toString() {
return '$runtimeType(begin: $begin, end: $end, progress: $progress)';
}
}
/// Geometry information for [Scaffold] components after layout is finished.
///
/// To get a [ValueNotifier] for the scaffold geometry of a given
/// [BuildContext], use [Scaffold.geometryOf].
///
/// The ScaffoldGeometry is only available during the paint phase, because
/// its value is computed during the animation and layout phases prior to painting.
///
/// For an example of using the [ScaffoldGeometry], see the [BottomAppBar],
/// which uses the [ScaffoldGeometry] to paint a notch around the
/// [FloatingActionButton].
///
/// For information about the [Scaffold]'s geometry that is used while laying
/// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry].
@immutable
class ScaffoldGeometry {
/// Create an object that describes the geometry of a [Scaffold].
......@@ -69,15 +205,13 @@ class ScaffoldGeometry {
this.floatingActionButtonNotch,
});
/// The distance from the scaffold's top edge to the top edge of the
/// rectangle in which the [Scaffold.bottomNavigationBar] bar is being laid
/// out.
/// The distance from the [Scaffold]'s top edge to the top edge of the
/// rectangle in which the [Scaffold.bottomNavigationBar] bar is laid out.
///
/// When there is no [Scaffold.bottomNavigationBar] set, this will be null.
/// Null if [Scaffold.bottomNavigationBar] is null.
final double bottomNavigationBarTop;
/// The rectangle in which the scaffold is laying out
/// [Scaffold.floatingActionButton].
/// The [Scaffold.floatingActionButton]'s bounding rectangle.
///
/// This is null when there is no floating action button showing.
final Rect floatingActionButtonArea;
......@@ -141,7 +275,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
: assert (context != null);
final BuildContext context;
double fabScale;
double floatingActionButtonScale;
ScaffoldGeometry geometry;
_Closeable computeNotchCloseable;
......@@ -157,7 +291,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
);
return true;
}());
return geometry._scaleFloatingActionButton(fabScale);
return geometry._scaleFloatingActionButton(floatingActionButtonScale);
}
void _updateWith({
......@@ -166,7 +300,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
double floatingActionButtonScale,
ComputeNotch floatingActionButtonNotch,
}) {
fabScale = floatingActionButtonScale ?? fabScale;
this.floatingActionButtonScale = floatingActionButtonScale ?? this.floatingActionButtonScale;
geometry = geometry.copyWith(
bottomNavigationBarTop: bottomNavigationBarTop,
floatingActionButtonArea: floatingActionButtonArea,
......@@ -194,19 +328,26 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
class _ScaffoldLayout extends MultiChildLayoutDelegate {
_ScaffoldLayout({
@required this.statusBarHeight,
@required this.bottomViewInset,
@required this.endPadding, // for floating action button
@required this.minInsets,
@required this.textDirection,
@required this.geometryNotifier,
});
final double statusBarHeight;
final double bottomViewInset;
final double endPadding;
// for floating action button
@required this.previousFloatingActionButtonLocation,
@required this.currentFloatingActionButtonLocation,
@required this.floatingActionButtonMoveAnimationProgress,
@required this.floatingActionButtonMotionAnimator,
}) : assert(previousFloatingActionButtonLocation != null),
assert(currentFloatingActionButtonLocation != null);
final EdgeInsets minInsets;
final TextDirection textDirection;
final _ScaffoldGeometryNotifier geometryNotifier;
final FloatingActionButtonLocation previousFloatingActionButtonLocation;
final FloatingActionButtonLocation currentFloatingActionButtonLocation;
final double floatingActionButtonMoveAnimationProgress;
final FloatingActionButtonAnimator floatingActionButtonMotionAnimator;
@override
void performLayout(Size size) {
final BoxConstraints looseConstraints = new BoxConstraints.loose(size);
......@@ -247,7 +388,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
// Set the content bottom to account for the greater of the height of any
// bottom-anchored material widgets or of the keyboard or other
// bottom-anchored system UI.
final double contentBottom = math.max(0.0, bottom - math.max(bottomViewInset, bottomWidgetsHeight));
final double contentBottom = math.max(0.0, bottom - math.max(minInsets.bottom, bottomWidgetsHeight));
if (hasChild(_ScaffoldSlot.body)) {
final BoxConstraints bodyConstraints = new BoxConstraints(
......@@ -265,10 +406,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
//
// If all three elements are present then either the center of the FAB straddles
// the top edge of the BottomSheet or the bottom of the FAB is
// _kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
// kFloatingActionButtonMargin above the SnackBar, whichever puts the FAB
// the farthest above the bottom of the parent. If only the FAB is has a
// non-zero height then it's inset from the parent's right and bottom edges
// by _kFloatingActionButtonMargin.
// by kFloatingActionButtonMargin.
Size bottomSheetSize = Size.zero;
Size snackBarSize = Size.zero;
......@@ -290,27 +431,32 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
Rect floatingActionButtonRect;
if (hasChild(_ScaffoldSlot.floatingActionButton)) {
final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
double fabX;
assert(textDirection != null);
switch (textDirection) {
case TextDirection.rtl:
fabX = _kFloatingActionButtonMargin + endPadding;
break;
case TextDirection.ltr:
fabX = size.width - fabSize.width - _kFloatingActionButtonMargin - endPadding;
break;
}
double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
if (snackBarSize.height > 0.0)
fabY = math.min(fabY, contentBottom - snackBarSize.height - fabSize.height - _kFloatingActionButtonMargin);
if (bottomSheetSize.height > 0.0)
fabY = math.min(fabY, contentBottom - bottomSheetSize.height - fabSize.height / 2.0);
positionChild(_ScaffoldSlot.floatingActionButton, new Offset(fabX, fabY));
floatingActionButtonRect = new Offset(fabX, fabY) & fabSize;
// To account for the FAB position being changed, we'll animate between
// the old and new positions.
final ScaffoldPrelayoutGeometry currentGeometry = new ScaffoldPrelayoutGeometry(
bottomSheetSize: bottomSheetSize,
contentBottom: contentBottom,
contentTop: contentTop,
floatingActionButtonSize: fabSize,
minInsets: minInsets,
scaffoldSize: size,
snackBarSize: snackBarSize,
textDirection: textDirection,
);
final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry);
final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry);
final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset(
begin: previousFabOffset,
end: currentFabOffset,
progress: floatingActionButtonMoveAnimationProgress,
);
positionChild(_ScaffoldSlot.floatingActionButton, fabOffset);
floatingActionButtonRect = fabOffset & fabSize;
}
if (hasChild(_ScaffoldSlot.statusBar)) {
layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: statusBarHeight));
layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top));
positionChild(_ScaffoldSlot.statusBar, Offset.zero);
}
......@@ -332,21 +478,36 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@override
bool shouldRelayout(_ScaffoldLayout oldDelegate) {
return oldDelegate.statusBarHeight != statusBarHeight
|| oldDelegate.bottomViewInset != bottomViewInset
|| oldDelegate.endPadding != endPadding
|| oldDelegate.textDirection != textDirection;
return oldDelegate.minInsets != minInsets
|| oldDelegate.textDirection != textDirection
|| oldDelegate.floatingActionButtonMoveAnimationProgress != floatingActionButtonMoveAnimationProgress
|| oldDelegate.previousFloatingActionButtonLocation != previousFloatingActionButtonLocation
|| oldDelegate.currentFloatingActionButtonLocation != currentFloatingActionButtonLocation;
}
}
/// Handler for scale and rotation animations in the [FloatingActionButton].
///
/// Currently, there are two types of [FloatingActionButton] animations:
///
/// * Entrance/Exit animations, which this widget triggers
/// when the [FloatingActionButton] is added, updated, or removed.
/// * Motion animations, which are triggered by the [Scaffold]
/// when its [FloatingActionButtonLocation] is updated.
class _FloatingActionButtonTransition extends StatefulWidget {
const _FloatingActionButtonTransition({
Key key,
this.child,
this.geometryNotifier,
}) : super(key: key);
@required this.child,
@required this.fabMoveAnimation,
@required this.fabMotionAnimator,
@required this.geometryNotifier,
}) : assert(fabMoveAnimation != null),
assert(fabMotionAnimator != null),
super(key: key);
final Widget child;
final Animation<double> fabMoveAnimation;
final FloatingActionButtonAnimator fabMotionAnimator;
final _ScaffoldGeometryNotifier geometryNotifier;
@override
......@@ -354,10 +515,16 @@ class _FloatingActionButtonTransition extends StatefulWidget {
}
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin {
// The animations applied to the Floating Action Button when it is entering or exiting.
// Controls the previous widget.child as it exits
AnimationController _previousController;
Animation<double> _previousScaleAnimation;
Animation<double> _previousRotationAnimation;
// Controls the current child widget.child as it exits
AnimationController _currentController;
CurvedAnimation _previousAnimation;
CurvedAnimation _currentAnimation;
// The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations.
Animation<double> _currentScaleAnimation;
Animation<double> _currentRotationAnimation;
Widget _previousChild;
@override
......@@ -365,24 +532,16 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
super.initState();
_previousController = new AnimationController(
duration: _kFloatingActionButtonSegue,
duration: kFloatingActionButtonSegue,
vsync: this,
)..addStatusListener(_handleAnimationStatusChanged);
_previousAnimation = new CurvedAnimation(
parent: _previousController,
curve: Curves.easeIn
);
_previousAnimation.addListener(_onProgressChanged);
)..addStatusListener(_handlePreviousAnimationStatusChanged);
_currentController = new AnimationController(
duration: _kFloatingActionButtonSegue,
duration: kFloatingActionButtonSegue,
vsync: this,
);
_currentAnimation = new CurvedAnimation(
parent: _currentController,
curve: Curves.easeIn
);
_currentAnimation.addListener(_onProgressChanged);
_updateAnimations();
if (widget.child != null) {
// If we start out with a child, have the child appear fully visible instead
......@@ -410,6 +569,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
final bool newChildIsNull = widget.child == null;
if (oldChildIsNull == newChildIsNull && oldWidget.child?.key == widget.child?.key)
return;
if (oldWidget.fabMotionAnimator != widget.fabMotionAnimator || oldWidget.fabMoveAnimation != oldWidget.fabMoveAnimation) {
// Get the right scale and rotation animations to use for this widget.
_updateAnimations();
}
if (_previousController.status == AnimationStatus.dismissed) {
final double currentValue = _currentController.value;
if (currentValue == 0.0 || oldWidget.child == null) {
......@@ -431,7 +594,43 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
}
}
void _handleAnimationStatusChanged(AnimationStatus status) {
void _updateAnimations() {
// Get the animations for exit and entrance.
final CurvedAnimation previousExitScaleAnimation = new CurvedAnimation(
parent: _previousController,
curve: Curves.easeIn,
);
final Animation<double> previousExitRotationAnimation = new Tween<double>(begin: 1.0, end: 1.0).animate(
new CurvedAnimation(parent: _previousController, curve: Curves.easeIn),
);
final CurvedAnimation currentEntranceScaleAnimation = new CurvedAnimation(
parent: _currentController,
curve: Curves.easeIn,
);
final Animation<double> currentEntranceRotationAnimation = new Tween<double>(
begin: 1.0 - kFloatingActionButtonTurnInterval,
end: 1.0,
).animate(
new CurvedAnimation(parent: _currentController, curve: Curves.easeIn),
);
// Get the animations for when the FAB is moving.
final Animation<double> moveScaleAnimation = widget.fabMotionAnimator.getScaleAnimation(parent: widget.fabMoveAnimation);
final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation);
// Aggregate the animations.
_previousScaleAnimation = new AnimationMin<double>(moveScaleAnimation, previousExitScaleAnimation);
_currentScaleAnimation = new AnimationMin<double>(moveScaleAnimation, currentEntranceScaleAnimation);
_previousRotationAnimation = new TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
_currentRotationAnimation = new TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
_currentScaleAnimation.addListener(_onProgressChanged);
_previousScaleAnimation.addListener(_onProgressChanged);
}
void _handlePreviousAnimationStatusChanged(AnimationStatus status) {
setState(() {
if (status == AnimationStatus.dismissed) {
assert(_currentController.status == AnimationStatus.dismissed);
......@@ -444,33 +643,27 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
@override
Widget build(BuildContext context) {
final List<Widget> children = <Widget>[];
if (_previousAnimation.status != AnimationStatus.dismissed) {
if (_previousController.status != AnimationStatus.dismissed) {
children.add(new ScaleTransition(
scale: _previousAnimation,
scale: _previousScaleAnimation,
child: new RotationTransition(
turns: _previousRotationAnimation,
child: _previousChild,
),
));
}
if (_currentAnimation.status != AnimationStatus.dismissed) {
children.add(new ScaleTransition(
scale: _currentAnimation,
scale: _currentScaleAnimation,
child: new RotationTransition(
turns: _kFloatingActionButtonTurnTween.animate(_currentAnimation),
turns: _currentRotationAnimation,
child: widget.child,
)
),
));
}
return new Stack(children: children);
}
void _onProgressChanged() {
if (_previousAnimation.status != AnimationStatus.dismissed) {
_updateGeometryScale(_previousAnimation.value);
return;
}
if (_currentAnimation.status != AnimationStatus.dismissed) {
_updateGeometryScale(_currentAnimation.value);
return;
}
_updateGeometryScale(math.max(_previousScaleAnimation.value, _currentScaleAnimation.value));
}
void _updateGeometryScale(double scale) {
......@@ -496,6 +689,11 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// of an app using the [bottomNavigationBar] property.
/// * [FloatingActionButton], which is a circular button typically shown in the
/// bottom right corner of the app using the [floatingActionButton] property.
/// * [FloatingActionButtonLocation], which is used to place the
/// [floatingActionButton] within the [Scaffold]'s layout.
/// * [FloatingActionButtonAnimator], which is used to animate the
/// [floatingActionButton] from one [floatingActionButtonLocation] to
/// another.
/// * [Drawer], which is a vertical panel that is typically displayed to the
/// left of the body (and often hidden on phones) using the [drawer]
/// property.
......@@ -517,6 +715,8 @@ class Scaffold extends StatefulWidget {
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
......@@ -552,6 +752,16 @@ class Scaffold extends StatefulWidget {
/// Typically a [FloatingActionButton].
final Widget floatingActionButton;
/// Responsible for determining where the [floatingActionButton] should go.
///
/// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat].
final FloatingActionButtonLocation floatingActionButtonLocation;
/// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation].
///
/// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling].
final FloatingActionButtonAnimator floatingActionButtonAnimator;
/// A set of buttons that are displayed at the bottom of the scaffold.
///
/// Typically this is a list of [FlatButton] widgets. These buttons are
......@@ -1040,6 +1250,32 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
return _currentBottomSheet;
}
// Floating Action Button API
AnimationController _floatingActionButtonMoveController;
FloatingActionButtonAnimator _floatingActionButtonAnimator;
FloatingActionButtonLocation _previousFloatingActionButtonLocation;
FloatingActionButtonLocation _floatingActionButtonLocation;
// Moves the Floating Action Button to the new Floating Action Button Location.
void _moveFloatingActionButton(final FloatingActionButtonLocation newLocation) {
FloatingActionButtonLocation previousLocation = _floatingActionButtonLocation;
double restartAnimationFrom = 0.0;
// If the Floating Action Button is moving right now, we need to start from a snapshot of the current transition.
if (_floatingActionButtonMoveController.isAnimating) {
previousLocation = new _TransitionSnapshotFabLocation(_previousFloatingActionButtonLocation, _floatingActionButtonLocation, _floatingActionButtonAnimator, _floatingActionButtonMoveController.value);
restartAnimationFrom = _floatingActionButtonAnimator.getAnimationRestart(_floatingActionButtonMoveController.value);
}
setState(() {
_previousFloatingActionButtonLocation = previousLocation;
_floatingActionButtonLocation = newLocation;
});
// Animate the motion even when the fab is null so that if the exit animation is running,
// the old fab will start the motion transition while it exits instead of jumping to the
// new position.
_floatingActionButtonMoveController.forward(from: restartAnimationFrom);
}
// iOS FEATURES - status bar tap, back gesture
......@@ -1059,7 +1295,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
}
}
// INTERNALS
_ScaffoldGeometryNotifier _geometryNotifier;
......@@ -1068,12 +1303,34 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
void initState() {
super.initState();
_geometryNotifier = new _ScaffoldGeometryNotifier(const ScaffoldGeometry(), context);
_floatingActionButtonLocation = widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation;
_floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator;
_previousFloatingActionButtonLocation = _floatingActionButtonLocation;
_floatingActionButtonMoveController = new AnimationController(
vsync: this,
lowerBound: 0.0,
upperBound: 1.0,
value: 1.0,
duration: kFloatingActionButtonSegue * 2,
);
}
@override
void didUpdateWidget(Scaffold oldWidget) {
// Update the Floating Action Button Animator, and then schedule the Floating Action Button for repositioning.
if (widget.floatingActionButtonAnimator != oldWidget.floatingActionButtonAnimator) {
_floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator;
}
if (widget.floatingActionButtonLocation != oldWidget.floatingActionButtonLocation) {
_moveFloatingActionButton(widget.floatingActionButtonLocation ?? _kDefaultFloatingActionButtonLocation);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_snackBarController?.dispose();
_snackBarController = null;
_snackBarTimer?.cancel();
_snackBarTimer = null;
_geometryNotifier.dispose();
......@@ -1081,6 +1338,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bottomSheet.animationController.dispose();
if (_currentBottomSheet != null)
_currentBottomSheet._widget.animationController.dispose();
_floatingActionButtonMoveController.dispose();
super.dispose();
}
......@@ -1241,6 +1499,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
children,
new _FloatingActionButtonTransition(
child: widget.floatingActionButton,
fabMoveAnimation: _floatingActionButtonMoveController,
fabMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
),
_ScaffoldSlot.floatingActionButton,
......@@ -1303,16 +1563,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
);
}
double endPadding;
switch (textDirection) {
case TextDirection.rtl:
endPadding = mediaQuery.padding.left;
break;
case TextDirection.ltr:
endPadding = mediaQuery.padding.right;
break;
}
assert(endPadding != null);
// The minimum insets for contents of the Scaffold to keep visible.
final EdgeInsets minInsets = mediaQuery.padding.copyWith(
bottom: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
);
return new _ScaffoldScope(
hasDrawer: hasDrawer,
......@@ -1321,16 +1575,20 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
controller: _primaryScrollController,
child: new Material(
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
child: new CustomMultiChildLayout(
child: new AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget child) {
return new CustomMultiChildLayout(
children: children,
delegate: new _ScaffoldLayout(
statusBarHeight: mediaQuery.padding.top,
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
endPadding: endPadding,
textDirection: textDirection,
minInsets: minInsets,
currentFloatingActionButtonLocation: _floatingActionButtonLocation,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation,
textDirection: textDirection,
),
),
);
}),
),
),
);
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Floating action button positioner', () {
Widget build(FloatingActionButton fab, FloatingActionButtonLocation fabLocation, [_GeometryListener listener]) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(
viewInsets: const EdgeInsets.only(bottom: 200.0),
),
child: new Scaffold(
appBar: new AppBar(title: const Text('FabLocation Test')),
floatingActionButtonLocation: fabLocation,
floatingActionButton: fab,
body: listener,
),
),
);
}
const FloatingActionButton fab1 = const FloatingActionButton(
onPressed: null,
child: const Text('1'),
);
testWidgets('still animates motion when the floating action button is null', (WidgetTester tester) async {
await tester.pumpWidget(build(null, null));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(null, FloatingActionButtonLocation.endFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(build(null, FloatingActionButtonLocation.centerFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
});
testWidgets('moves fab from center to end and back', (WidgetTester tester) async {
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('moves to and from custom-defined positions', (WidgetTester tester) async {
await tester.pumpWidget(build(fab1, _kTopStartFabLocation));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(fab1, _kTopStartFabLocation));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('interrupts in-progress animations without jumps', (WidgetTester tester) async {
final _GeometryListener geometryListener = new _GeometryListener();
ScaffoldGeometry geometry;
_GeometryListenerState listenerState;
Size previousRect;
// The maximum amounts we expect the fab width and height to change during one step of a transition.
const double maxDeltaWidth = 12.0;
const double maxDeltaHeight = 12.0;
// Measure the delta in width and height of the fab, and check that it never grows
// by more than the expected maximum deltas.
void check() {
geometry = listenerState.cache.value;
final Size currentRect = geometry.floatingActionButtonArea?.size;
// Measure the delta in width and height of the rect, and check that it never grows
// by more than a safe amount.
if (previousRect != null && currentRect != null) {
final double deltaWidth = currentRect.width - previousRect.width;
final double deltaHeight = currentRect.height - previousRect.height;
expect(deltaWidth.abs(), lessThanOrEqualTo(maxDeltaWidth), reason: "The Floating Action Button's width should not change faster than $maxDeltaWidth per animation step.");
expect(deltaHeight.abs(), lessThanOrEqualTo(maxDeltaHeight), reason: "The Floating Action Button's width should not change faster than $maxDeltaHeight per animation step.");
}
previousRect = currentRect;
}
// We'll listen to the Scaffold's geometry for any 'jumps' to a size of 1 to detect changes in the size and rotation of the fab.
// Creating a scaffold with the fab at endFloat
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat, geometryListener));
listenerState = tester.state(find.byType(_GeometryListener));
listenerState.geometryListenable.addListener(check);
// Moving the fab to centerFloat'
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.centerFloat, geometryListener));
await tester.pumpAndSettle();
// Moving the fab to the top start after finishing the previous motion
await tester.pumpWidget(build(fab1, _kTopStartFabLocation, geometryListener));
// Interrupting motion to move to the end float
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat, geometryListener));
await tester.pumpAndSettle();
});
});
}
class _GeometryListener extends StatefulWidget {
@override
State createState() => new _GeometryListenerState();
}
class _GeometryListenerState extends State<_GeometryListener> {
@override
Widget build(BuildContext context) {
return new CustomPaint(
painter: cache
);
}
int numNotifications = 0;
ValueListenable<ScaffoldGeometry> geometryListenable;
_GeometryCachePainter cache;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
if (geometryListenable != null)
geometryListenable.removeListener(onGeometryChanged);
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = new _GeometryCachePainter(geometryListenable);
}
void onGeometryChanged() {
numNotifications += 1;
}
}
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class _GeometryCachePainter extends CustomPainter {
_GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
ScaffoldGeometry value;
@override
void paint(Canvas canvas, Size size) {
value = geometryListenable.value;
}
@override
bool shouldRepaint(_GeometryCachePainter oldDelegate) {
return true;
}
}
const _TopStartFabLocation _kTopStartFabLocation = const _TopStartFabLocation();
class _TopStartFabLocation extends FloatingActionButtonLocation {
const _TopStartFabLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final double fabX = 16.0 + scaffoldGeometry.minInsets.left;
final double fabY = scaffoldGeometry.contentTop - (scaffoldGeometry.floatingActionButtonSize.height / 2.0);
return new Offset(fabX, fabY);
}
}
\ No newline at end of file
......@@ -109,7 +109,7 @@ void main() {
expect(bodyBox.size, equals(const Size(800.0, 0.0)));
});
testWidgets('Floating action animation', (WidgetTester tester) async {
testWidgets('Floating action entrance/exit animation', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(home: const Scaffold(
floatingActionButton: const FloatingActionButton(
key: const Key('one'),
......@@ -131,7 +131,9 @@ void main() {
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(new Container());
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new MaterialApp(home: const Scaffold()));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new MaterialApp(home: const Scaffold(
......@@ -145,7 +147,7 @@ void main() {
expect(tester.binding.transientCallbackCount, greaterThan(0));
});
testWidgets('Floating action button position', (WidgetTester tester) async {
testWidgets('Floating action button directionality', (WidgetTester tester) async {
Widget build(TextDirection textDirection) {
return new Directionality(
textDirection: textDirection,
......@@ -168,6 +170,7 @@ void main() {
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
await tester.pumpWidget(build(TextDirection.rtl));
expect(tester.binding.transientCallbackCount, 0);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 356.0));
});
......@@ -779,13 +782,13 @@ void main() {
bottomNavigationBar: new ConstrainedBox(
key: key,
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)));
final RenderBox navigationBox = tester.renderObject(find.byKey(key));
final RenderBox appBox = tester.renderObject(find.byType(MaterialApp));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
......@@ -798,11 +801,11 @@ void main() {
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
......@@ -817,13 +820,13 @@ void main() {
body: new Container(),
floatingActionButton: new FloatingActionButton(
key: key,
child: new GeometryListener(),
child: new _GeometryListener(),
onPressed: () {},
),
)));
final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;
......@@ -838,11 +841,11 @@ void main() {
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
......@@ -851,12 +854,12 @@ void main() {
);
});
testWidgets('floatingActionButton animation', (WidgetTester tester) async {
testWidgets('floatingActionButton entrance/exit animation', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)));
......@@ -864,12 +867,12 @@ void main() {
body: new Container(),
floatingActionButton: new FloatingActionButton(
key: key,
child: new GeometryListener(),
child: new _GeometryListener(),
onPressed: () {},
),
)));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
await tester.pump(const Duration(milliseconds: 50));
ScaffoldGeometry geometry = listenerState.cache.value;
......@@ -908,11 +911,11 @@ void main() {
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
......@@ -921,7 +924,7 @@ void main() {
body: new Container(),
floatingActionButton: new FloatingActionButton(
key: key,
child: new GeometryListener(),
child: new _GeometryListener(),
onPressed: () {},
),
)));
......@@ -946,13 +949,13 @@ void main() {
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(computeNotch),
floatingActionButton: new _ComputeNotchSetter(computeNotch),
)
));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
ScaffoldGeometry geometry = listenerState.cache.value;
expect(
......@@ -964,7 +967,7 @@ void main() {
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
)
));
......@@ -985,13 +988,13 @@ void main() {
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(computeNotch),
floatingActionButton: new _ComputeNotchSetter(computeNotch),
)
));
final ComputeNotchSetterState computeNotchSetterState = tester.state(find.byType(ComputeNotchSetter));
final _ComputeNotchSetterState computeNotchSetterState = tester.state(find.byType(_ComputeNotchSetter));
final VoidCallback clearFirstComputeNotch = computeNotchSetterState.clearComputeNotch;
......@@ -1000,9 +1003,9 @@ void main() {
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
child: new _GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(
floatingActionButton: new _ComputeNotchSetter(
computeNotch2,
// We're setting a key to make sure a new ComputeNotchSetterState is
// created.
......@@ -1019,7 +1022,7 @@ void main() {
clearFirstComputeNotch();
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
......@@ -1030,12 +1033,12 @@ void main() {
});
}
class GeometryListener extends StatefulWidget {
class _GeometryListener extends StatefulWidget {
@override
State createState() => new GeometryListenerState();
_GeometryListenerState createState() => new _GeometryListenerState();
}
class GeometryListenerState extends State<GeometryListener> {
class _GeometryListenerState extends State<_GeometryListener> {
@override
Widget build(BuildContext context) {
return new CustomPaint(
......@@ -1045,7 +1048,7 @@ class GeometryListenerState extends State<GeometryListener> {
int numNotifications = 0;
ValueListenable<ScaffoldGeometry> geometryListenable;
GeometryCachePainter cache;
_GeometryCachePainter cache;
@override
void didChangeDependencies() {
......@@ -1059,7 +1062,7 @@ class GeometryListenerState extends State<GeometryListener> {
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = new GeometryCachePainter(geometryListenable);
cache = new _GeometryCachePainter(geometryListenable);
}
void onGeometryChanged() {
......@@ -1070,8 +1073,8 @@ class GeometryListenerState extends State<GeometryListener> {
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class GeometryCachePainter extends CustomPainter {
GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
class _GeometryCachePainter extends CustomPainter {
_GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
......@@ -1082,21 +1085,21 @@ class GeometryCachePainter extends CustomPainter {
}
@override
bool shouldRepaint(GeometryCachePainter oldDelegate) {
bool shouldRepaint(_GeometryCachePainter oldDelegate) {
return true;
}
}
class ComputeNotchSetter extends StatefulWidget {
const ComputeNotchSetter(this.computeNotch, {Key key}): super(key: key);
class _ComputeNotchSetter extends StatefulWidget {
const _ComputeNotchSetter(this.computeNotch, {Key key}): super(key: key);
final ComputeNotch computeNotch;
@override
State createState() => new ComputeNotchSetterState();
State createState() => new _ComputeNotchSetterState();
}
class ComputeNotchSetterState extends State<ComputeNotchSetter> {
class _ComputeNotchSetterState extends State<_ComputeNotchSetter> {
VoidCallback clearComputeNotch;
@override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment