Unverified Commit f3c742c8 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added BottomAppBar docked FloatingActionButtonLocations (#16167)

* Added BottomAppBar docked FloationActionButtonLocations

* Moved the startTop FloatingActionButtonLocation to the demo

* fixed a typo
parent a80d557b
......@@ -19,9 +19,9 @@ class FabMotionDemo extends StatefulWidget {
class _FabMotionDemoState extends State<FabMotionDemo> {
static const List<FloatingActionButtonLocation> _floatingActionButtonLocations = const <FloatingActionButtonLocation>[
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.centerFloat,
const _TopStartFloatingActionButtonLocation(),
const _StartTopFloatingActionButtonLocation(),
];
bool _showFab = true;
......@@ -29,25 +29,25 @@ class _FabMotionDemoState extends State<FabMotionDemo> {
@override
Widget build(BuildContext context) {
final Widget floatingActionButton = _showFab
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),
child: const Icon(Icons.add),
);
})
})
: null;
return new Scaffold(
appBar: new AppBar(
title: const Text('FAB Location'),
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),
preferredSize: const Size.fromHeight(48.0),
child: const SizedBox(),
),
),
......@@ -93,8 +93,8 @@ class _FabMotionDemoState extends State<FabMotionDemo> {
// 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();
class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _StartTopFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
......@@ -130,7 +130,7 @@ class _TopStartFloatingActionButtonLocation extends FloatingActionButtonLocation
fabX = startPadding;
break;
}
// Finally, we'll place the Y coordinate for the Floating Action Button
// 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
......
......@@ -12,37 +12,37 @@ 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;
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]
///
/// 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
/// [Scaffold.floatingActionButton] from one [FloatingActionButtonLocation] to
/// another.
/// * [ScaffoldPrelayoutGeometry], the geometry that
/// * [ScaffoldPrelayoutGeometry], the geometry that
/// [FloatingActionButtonLocation]s use to position the [FloatingActionButton].
abstract class FloatingActionButtonLocation {
/// Abstract const constructor. This constructor enables subclasses to provide
......@@ -50,15 +50,39 @@ abstract class FloatingActionButtonLocation {
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();
/// End-aligned [FloatingActionButton], floating over the
/// [Scaffold.bottomNavigationBar] so that the center of the floating
/// action button lines up with the top of the bottom navigation bar.
///
/// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar],
/// the bottom app bar can include a "notch" in its shape that accommodates
/// the overlapping floating action button.
///
/// This is unlikely to be a useful location for apps that lack a bottom
/// navigation bar.
static FloatingActionButtonLocation endDocked = const _EndDockedFloatingActionButtonLocation();
/// Center-aligned [FloatingActionButton], floating over the
/// [Scaffold.bottomNavigationBar] so that the center of the floating
/// action button lines up with the top of the bottom navigation bar.
///
/// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar],
/// the bottom app bar can include a "notch" in its shape that accomodates
/// the overlapping floating action button.
///
/// This is unlikely to be a useful location for apps that lack a bottom
/// navigation bar.
static FloatingActionButtonLocation centerDocked = const _CenterDockedFloatingActionButtonLocation();
/// 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]
......@@ -130,8 +154,70 @@ class _EndFloatFabLocation extends FloatingActionButtonLocation {
}
}
// Provider of common logic for [FloatingActionButtonLocation]s that
// dock to the [BottomAppBar].
abstract class _DockedFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _DockedFloatingActionButtonLocation();
// Positions the Y coordinate of the [FloatingActionButton] at a height
// where it docks to the [BottomAppBar].
@protected
double getDockedY(ScaffoldPrelayoutGeometry scaffoldGeometry) {
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 / 2.0;
// The FAB should sit with a margin between it and the snack bar.
if (snackBarHeight > 0.0)
fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
// The FAB should sit with its center in front of the top of the bottom sheet.
if (bottomSheetHeight > 0.0)
fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);
final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight;
return math.min(maxFabY, fabY);
}
}
class _EndDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation {
const _EndDockedFloatingActionButtonLocation();
@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;
}
// Return an offset with a docked Y coordinate.
return new Offset(fabX, getDockedY(scaffoldGeometry));
}
}
class _CenterDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation {
const _CenterDockedFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0;
return new Offset(fabX, getDockedY(scaffoldGeometry));
}
}
/// 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
......@@ -139,48 +225,48 @@ class _EndFloatFabLocation extends FloatingActionButtonLocation {
/// * 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.
/// * [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
/// 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
///
/// [begin] is the [Offset] provided by the previous
/// [FloatingActionButtonLocation].
///
/// [end] is the [Offset] provided by the new
///
/// [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}) {
......@@ -194,15 +280,15 @@ abstract class FloatingActionButtonAnimator {
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
///
/// For example, to create a rotation animation that rotates the
/// [FloatingActionButton] through a full circle:
///
///
/// ```dart
/// @override
/// Animation<double> getRotationAnimation({@required Animation<double> parent}) {
......@@ -212,15 +298,15 @@ abstract class FloatingActionButtonAnimator {
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;
......@@ -256,10 +342,10 @@ class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
@override
Animation<double> getRotationAnimation({Animation<double> parent}) {
// Because we only see the last half of the rotation tween,
// 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,
begin: 1.0 - kFloatingActionButtonTurnInterval * 2,
end: 1.0,
);
// This rotation will turn on the way in, but not on the way out.
......@@ -294,4 +380,4 @@ class _AnimationSwap<T> extends CompoundAnimation<T> {
@override
T get value => parent.value < swapThreshold ? first.value : next.value;
}
\ No newline at end of file
}
......@@ -7,53 +7,31 @@ 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'),
);
group('Basic floating action button locations', () {
testWidgets('still animates motion when the floating action button is null', (WidgetTester tester) async {
await tester.pumpWidget(build(null, null));
await tester.pumpWidget(buildFrame(fab: null, location: null));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(null, FloatingActionButtonLocation.endFloat));
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.endFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(build(null, FloatingActionButtonLocation.centerFloat));
await tester.pumpWidget(buildFrame(fab: null, location: 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));
await tester.pumpWidget(buildFrame(location: 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));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
......@@ -62,8 +40,8 @@ void main() {
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
......@@ -73,11 +51,11 @@ void main() {
});
testWidgets('moves to and from custom-defined positions', (WidgetTester tester) async {
await tester.pumpWidget(build(fab1, _kTopStartFabLocation));
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.centerFloat));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
......@@ -85,8 +63,8 @@ void main() {
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(build(fab1, _kTopStartFabLocation));
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
......@@ -122,23 +100,76 @@ void main() {
// 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));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: 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.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pumpAndSettle();
// Moving the fab to the top start after finishing the previous motion
await tester.pumpWidget(build(fab1, _kTopStartFabLocation, geometryListener));
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener));
// Interrupting motion to move to the end float
await tester.pumpWidget(build(fab1, FloatingActionButtonLocation.endFloat, geometryListener));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
});
testWidgets('Docked floating action button locations', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
// Scaffold 800x600, FAB is 56x56, BAB is 800x100, FAB's center is
// at the top of the BAB.
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.centerDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
});
testWidgets('Docked floating action button locations: no BAB, small BAB', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 16.0),
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
});
}
......@@ -169,7 +200,7 @@ class _GeometryListenerState extends State<_GeometryListener> {
if (geometryListenable != null)
geometryListenable.removeListener(onGeometryChanged);
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = new _GeometryCachePainter(geometryListenable);
......@@ -201,15 +232,50 @@ class _GeometryCachePainter extends CustomPainter {
}
}
const _TopStartFabLocation _kTopStartFabLocation = const _TopStartFabLocation();
Widget buildFrame({
FloatingActionButton fab: const FloatingActionButton(
onPressed: null,
child: const Text('1'),
),
FloatingActionButtonLocation location,
_GeometryListener listener,
TextDirection textDirection: TextDirection.ltr,
EdgeInsets viewInsets: const EdgeInsets.only(bottom: 200.0),
Widget bab,
}) {
return new Directionality(
textDirection: textDirection,
child: new MediaQuery(
data: new MediaQueryData(viewInsets: viewInsets),
child: new Scaffold(
appBar: new AppBar(title: const Text('FabLocation Test')),
floatingActionButtonLocation: location,
floatingActionButton: fab,
bottomNavigationBar: bab,
body: listener,
),
),
);
}
class _TopStartFabLocation extends FloatingActionButtonLocation {
const _TopStartFabLocation();
class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _StartTopFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final double fabX = 16.0 + scaffoldGeometry.minInsets.left;
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - startPadding;
break;
case TextDirection.ltr:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left;
fabX = startPadding;
break;
}
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