mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
600739d195
commit
dd0acea1ec
147
examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart
Normal file
147
examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart
Normal file
@ -0,0 +1,147 @@
|
||||
// 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';
|
||||
|
@ -0,0 +1,297 @@
|
||||
// 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;
|
||||
}
|
@ -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,
|
||||
});
|
||||
// for floating action button
|
||||
@required this.previousFloatingActionButtonLocation,
|
||||
@required this.currentFloatingActionButtonLocation,
|
||||
@required this.floatingActionButtonMoveAnimationProgress,
|
||||
@required this.floatingActionButtonMotionAnimator,
|
||||
}) : assert(previousFloatingActionButtonLocation != null),
|
||||
assert(currentFloatingActionButtonLocation != null);
|
||||
|
||||
final double statusBarHeight;
|
||||
final double bottomViewInset;
|
||||
final double endPadding;
|
||||
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,
|
||||
child: _previousChild,
|
||||
));
|
||||
}
|
||||
if (_currentAnimation.status != AnimationStatus.dismissed) {
|
||||
children.add(new ScaleTransition(
|
||||
scale: _currentAnimation,
|
||||
scale: _previousScaleAnimation,
|
||||
child: new RotationTransition(
|
||||
turns: _kFloatingActionButtonTurnTween.animate(_currentAnimation),
|
||||
child: widget.child,
|
||||
)
|
||||
turns: _previousRotationAnimation,
|
||||
child: _previousChild,
|
||||
),
|
||||
));
|
||||
}
|
||||
children.add(new ScaleTransition(
|
||||
scale: _currentScaleAnimation,
|
||||
child: new RotationTransition(
|
||||
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,17 +1563,11 @@ 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,
|
||||
geometryNotifier: _geometryNotifier,
|
||||
@ -1321,16 +1575,20 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
||||
controller: _primaryScrollController,
|
||||
child: new Material(
|
||||
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
|
||||
child: new CustomMultiChildLayout(
|
||||
children: children,
|
||||
delegate: new _ScaffoldLayout(
|
||||
statusBarHeight: mediaQuery.padding.top,
|
||||
bottomViewInset: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
|
||||
endPadding: endPadding,
|
||||
textDirection: textDirection,
|
||||
geometryNotifier: _geometryNotifier,
|
||||
),
|
||||
),
|
||||
child: new AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget child) {
|
||||
return new CustomMultiChildLayout(
|
||||
children: children,
|
||||
delegate: new _ScaffoldLayout(
|
||||
minInsets: minInsets,
|
||||
currentFloatingActionButtonLocation: _floatingActionButtonLocation,
|
||||
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
|
||||
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
|
||||
geometryNotifier: _geometryNotifier,
|
||||
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation,
|
||||
textDirection: textDirection,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,215 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user