From f3c742c8a68825663cd89c3953aa250bee794d27 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 3 Apr 2018 14:25:06 -0700 Subject: [PATCH] Added BottomAppBar docked FloatingActionButtonLocations (#16167) * Added BottomAppBar docked FloationActionButtonLocations * Moved the startTop FloatingActionButtonLocation to the demo * fixed a typo --- .../lib/demo/material/fab_motion_demo.dart | 20 +-- .../floating_action_button_location.dart | 166 +++++++++++++----- .../floating_action_button_location_test.dart | 158 ++++++++++++----- 3 files changed, 248 insertions(+), 96 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart b/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart index 5c92bc34e21..1f265aae374 100644 --- a/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart @@ -19,9 +19,9 @@ class FabMotionDemo extends StatefulWidget { class _FabMotionDemoState extends State { static const List _floatingActionButtonLocations = const [ - FloatingActionButtonLocation.endFloat, + FloatingActionButtonLocation.endFloat, FloatingActionButtonLocation.centerFloat, - const _TopStartFloatingActionButtonLocation(), + const _StartTopFloatingActionButtonLocation(), ]; bool _showFab = true; @@ -29,25 +29,25 @@ class _FabMotionDemoState extends State { @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 { // 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 diff --git a/packages/flutter/lib/src/material/floating_action_button_location.dart b/packages/flutter/lib/src/material/floating_action_button_location.dart index 7634f9fb507..8e139cb0aef 100644 --- a/packages/flutter/lib/src/material/floating_action_button_location.dart +++ b/packages/flutter/lib/src/material/floating_action_button_location.dart @@ -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 getScaleAnimation({@required Animation parent}) { @@ -194,15 +280,15 @@ abstract class FloatingActionButtonAnimator { Animation getScaleAnimation({@required Animation 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 getRotationAnimation({@required Animation parent}) { @@ -212,15 +298,15 @@ abstract class FloatingActionButtonAnimator { Animation getRotationAnimation({@required Animation 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 getRotationAnimation({Animation 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 rotationTween = new Tween( - 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 extends CompoundAnimation { @override T get value => parent.value < swapThreshold ? first.value : next.value; -} \ No newline at end of file +} diff --git a/packages/flutter/test/material/floating_action_button_location_test.dart b/packages/flutter/test/material/floating_action_button_location_test.dart index a868a2c7cd2..96609a38ef7 100644 --- a/packages/flutter/test/material/floating_action_button_location_test.dart +++ b/packages/flutter/test/material/floating_action_button_location_test.dart @@ -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 +}