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.
diff --git a/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart b/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart
new file mode 100644
index 0000000..5c92bc3
--- /dev/null
+++ b/examples/flutter_gallery/lib/demo/material/fab_motion_demo.dart
@@ -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);
+  }
+}
diff --git a/examples/flutter_gallery/lib/demo/material/material.dart b/examples/flutter_gallery/lib/demo/material/material.dart
index 975e552..ddf46e3 100644
--- a/examples/flutter_gallery/lib/demo/material/material.dart
+++ b/examples/flutter_gallery/lib/demo/material/material.dart
@@ -11,6 +11,7 @@
 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';
diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart
index 18bdea7..5237367 100644
--- a/examples/flutter_gallery/lib/gallery/item.dart
+++ b/examples/flutter_gallery/lib/gallery/item.dart
@@ -159,6 +159,13 @@
       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',
       category: 'Material Components',
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index 658e694..fe38f19 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -49,6 +49,7 @@
 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';
diff --git a/packages/flutter/lib/src/material/floating_action_button_location.dart b/packages/flutter/lib/src/material/floating_action_button_location.dart
new file mode 100644
index 0000000..7634f9f
--- /dev/null
+++ b/packages/flutter/lib/src/material/floating_action_button_location.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;
+}
\ No newline at end of file
diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart
index 8aea589..c5036b3 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -8,6 +8,7 @@
 
 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 '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 @@
   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 @@
     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 @@
     : assert (context != null);
 
   final BuildContext context;
-  double fabScale;
+  double floatingActionButtonScale;
   ScaffoldGeometry geometry;
   _Closeable computeNotchCloseable;
 
@@ -157,7 +291,7 @@
         );
       return true;
     }());
-    return geometry._scaleFloatingActionButton(fabScale);
+    return geometry._scaleFloatingActionButton(floatingActionButtonScale);
   }
 
   void _updateWith({
@@ -166,7 +300,7 @@
     double floatingActionButtonScale,
     ComputeNotch floatingActionButtonNotch,
   }) {
-    fabScale = floatingActionButtonScale ?? fabScale;
+    this.floatingActionButtonScale = floatingActionButtonScale ?? this.floatingActionButtonScale;
     geometry = geometry.copyWith(
       bottomNavigationBarTop: bottomNavigationBarTop,
       floatingActionButtonArea: floatingActionButtonArea,
@@ -194,19 +328,26 @@
 
 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 @@
     // 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 @@
     //
     // 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 @@
     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 @@
 
   @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 _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 @@
     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 @@
     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 @@
     }
   }
 
-  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 @@
   @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 @@
 ///    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 @@
     this.appBar,
     this.body,
     this.floatingActionButton,
+    this.floatingActionButtonLocation,
+    this.floatingActionButtonAnimator,
     this.persistentFooterButtons,
     this.drawer,
     this.endDrawer,
@@ -552,6 +752,16 @@
   /// 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 @@
     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 @@
     }
   }
 
-
   // INTERNALS
 
   _ScaffoldGeometryNotifier _geometryNotifier;
@@ -1068,12 +1303,34 @@
   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 @@
       bottomSheet.animationController.dispose();
     if (_currentBottomSheet != null)
       _currentBottomSheet._widget.animationController.dispose();
+    _floatingActionButtonMoveController.dispose();
     super.dispose();
   }
 
@@ -1241,6 +1499,8 @@
       children,
       new _FloatingActionButtonTransition(
         child: widget.floatingActionButton,
+        fabMoveAnimation: _floatingActionButtonMoveController,
+        fabMotionAnimator: _floatingActionButtonAnimator,
         geometryNotifier: _geometryNotifier,
       ),
       _ScaffoldSlot.floatingActionButton,
@@ -1303,17 +1563,11 @@
       );
     }
 
-    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 @@
         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,
+              ),
+            );
+          }),
         ),
       ),
     );
diff --git a/packages/flutter/test/material/floating_action_button_location_test.dart b/packages/flutter/test/material/floating_action_button_location_test.dart
new file mode 100644
index 0000000..a868a2c
--- /dev/null
+++ b/packages/flutter/test/material/floating_action_button_location_test.dart
@@ -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);
+  }
+}
\ No newline at end of file
diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart
index 72ea8f8..62af36b 100644
--- a/packages/flutter/test/material/scaffold_test.dart
+++ b/packages/flutter/test/material/scaffold_test.dart
@@ -109,7 +109,7 @@
     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 @@
     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 @@
     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 @@
     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 @@
             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 @@
       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 @@
             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 @@
       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 @@
       );
     });
 
-    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 @@
             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 @@
       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 @@
             body: new Container(),
             floatingActionButton: new FloatingActionButton(
               key: key,
-              child: new GeometryListener(),
+              child: new _GeometryListener(),
               onPressed: () {},
             ),
       )));
@@ -946,13 +949,13 @@
           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 @@
           home: new Scaffold(
             body: new ConstrainedBox(
               constraints: const BoxConstraints.expand(height: 80.0),
-              child: new GeometryListener(),
+              child: new _GeometryListener(),
             ),
           )
       ));
@@ -985,13 +988,13 @@
           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 @@
           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 @@
 
       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 @@
   });
 }
 
-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 @@
 
   int numNotifications = 0;
   ValueListenable<ScaffoldGeometry> geometryListenable;
-  GeometryCachePainter cache;
+  _GeometryCachePainter cache;
 
   @override
   void didChangeDependencies() {
@@ -1059,7 +1062,7 @@
 
     geometryListenable = newListenable;
     geometryListenable.addListener(onGeometryChanged);
-    cache = new GeometryCachePainter(geometryListenable);
+    cache = new _GeometryCachePainter(geometryListenable);
   }
 
   void onGeometryChanged() {
@@ -1070,8 +1073,8 @@
 // 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 @@
   }
 
   @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