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