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
......@@ -21,7 +21,7 @@ class _FabMotionDemoState extends State<FabMotionDemo> {
static const List<FloatingActionButtonLocation> _floatingActionButtonLocations = const <FloatingActionButtonLocation>[
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.centerFloat,
const _TopStartFloatingActionButtonLocation(),
const _StartTopFloatingActionButtonLocation(),
];
bool _showFab = true;
......@@ -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) {
......
......@@ -57,6 +57,30 @@ abstract class FloatingActionButtonLocation {
/// 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
......@@ -130,6 +154,68 @@ 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:
......
......@@ -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,7 +40,7 @@ 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));
......@@ -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,7 +63,7 @@ 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));
......@@ -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));
});
}
......@@ -201,14 +232,49 @@ 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);
}
......
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