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 '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
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