Unverified Commit 3a93061e authored by Hans Muller's avatar Hans Muller Committed by GitHub

Extended Floating Action Button (#15841)

parent 1f5d9041
...@@ -44,6 +44,7 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat ...@@ -44,6 +44,7 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat
TabController _controller; TabController _controller;
_Page _selectedPage; _Page _selectedPage;
bool _extendedButtons;
@override @override
void initState() { void initState() {
...@@ -101,6 +102,30 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat ...@@ -101,6 +102,30 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat
); );
} }
Widget buildFloatingActionButton(_Page page) {
if (!page.fabDefined)
return null;
if (_extendedButtons) {
return new FloatingActionButton.extended(
key: new ValueKey<Key>(page.fabKey),
tooltip: 'Show explanation',
backgroundColor: page.fabColor,
icon: page.fabIcon,
label: new Text(page.label.toUpperCase()),
onPressed: _showExplanatoryText
);
}
return new FloatingActionButton(
key: page.fabKey,
tooltip: 'Show explanation',
backgroundColor: page.fabColor,
child: page.fabIcon,
onPressed: _showExplanatoryText
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
...@@ -110,15 +135,19 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat ...@@ -110,15 +135,19 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat
bottom: new TabBar( bottom: new TabBar(
controller: _controller, controller: _controller,
tabs: _allPages.map((_Page page) => new Tab(text: page.label.toUpperCase())).toList(), tabs: _allPages.map((_Page page) => new Tab(text: page.label.toUpperCase())).toList(),
) ),
), actions: <Widget>[
floatingActionButton: !_selectedPage.fabDefined ? null : new FloatingActionButton( new IconButton(
key: _selectedPage.fabKey, icon: const Icon(Icons.sentiment_very_satisfied),
tooltip: 'Show explanation', onPressed: () {
backgroundColor: _selectedPage.fabColor, setState(() {
child: _selectedPage.fabIcon, _extendedButtons = !_extendedButtons;
onPressed: _showExplanatoryText });
},
),
],
), ),
floatingActionButton: buildFloatingActionButton(_selectedPage),
body: new TabBarView( body: new TabBarView(
controller: _controller, controller: _controller,
children: _allPages.map(buildTabView).toList() children: _allPages.map(buildTabView).toList()
......
...@@ -5,19 +5,28 @@ ...@@ -5,19 +5,28 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'button.dart';
import 'ink_well.dart';
import 'material.dart';
import 'scaffold.dart'; import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
// TODO(eseidel): This needs to change based on device size? const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor(
// http://material.google.com/layout/metrics-keylines.html#metrics-keylines-keylines-spacing width: 56.0,
const double _kSize = 56.0; height: 56.0,
const double _kSizeMini = 40.0; );
const BoxConstraints _kMiniSizeConstraints = const BoxConstraints.tightFor(
width: 40.0,
height: 40.0,
);
const BoxConstraints _kExtendedSizeConstraints = const BoxConstraints(
minHeight: 48.0,
maxHeight: 48.0,
);
class _DefaultHeroTag { class _DefaultHeroTag {
const _DefaultHeroTag(); const _DefaultHeroTag();
...@@ -52,13 +61,15 @@ class _DefaultHeroTag { ...@@ -52,13 +61,15 @@ class _DefaultHeroTag {
/// * [FlatButton] /// * [FlatButton]
/// * <https://material.google.com/components/buttons-floating-action-button.html> /// * <https://material.google.com/components/buttons-floating-action-button.html>
class FloatingActionButton extends StatefulWidget { class FloatingActionButton extends StatefulWidget {
/// Creates a floating action button. /// Creates a circular floating action button.
/// ///
/// Most commonly used in the [Scaffold.floatingActionButton] field. /// The [elevation], [highlightElevation], [mini], [notchMargin], and [shape]
/// arguments must not be null.
const FloatingActionButton({ const FloatingActionButton({
Key key, Key key,
this.child, this.child,
this.tooltip, this.tooltip,
this.foregroundColor,
this.backgroundColor, this.backgroundColor,
this.heroTag: const _DefaultHeroTag(), this.heroTag: const _DefaultHeroTag(),
this.elevation: 6.0, this.elevation: 6.0,
...@@ -66,7 +77,54 @@ class FloatingActionButton extends StatefulWidget { ...@@ -66,7 +77,54 @@ class FloatingActionButton extends StatefulWidget {
@required this.onPressed, @required this.onPressed,
this.mini: false, this.mini: false,
this.notchMargin: 4.0, this.notchMargin: 4.0,
}) : super(key: key); this.shape: const CircleBorder(),
this.isExtended: false,
}) : assert(elevation != null),
assert(highlightElevation != null),
assert(mini != null),
assert(notchMargin != null),
assert(shape != null),
assert(isExtended != null),
_sizeConstraints = mini ? _kMiniSizeConstraints : _kSizeConstraints,
super(key: key);
/// Creates a wider [StadiumBorder] shaped floating action button with both
/// an [icon] and a [label].
///
/// The [label], [icon], [elevation], [highlightElevation]
/// [notchMargin], and [shape] arguments must not be null.
FloatingActionButton.extended({
Key key,
this.tooltip,
this.foregroundColor,
this.backgroundColor,
this.heroTag: const _DefaultHeroTag(),
this.elevation: 6.0,
this.highlightElevation: 12.0,
@required this.onPressed,
this.notchMargin: 4.0,
this.shape: const StadiumBorder(),
this.isExtended: true,
@required Widget icon,
@required Widget label,
}) : assert(elevation != null),
assert(highlightElevation != null),
assert(notchMargin != null),
assert(shape != null),
assert(isExtended != null),
_sizeConstraints = _kExtendedSizeConstraints,
mini = false,
child = new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(width: 16.0),
icon,
const SizedBox(width: 8.0),
label,
const SizedBox(width: 20.0),
],
),
super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -79,9 +137,14 @@ class FloatingActionButton extends StatefulWidget { ...@@ -79,9 +137,14 @@ class FloatingActionButton extends StatefulWidget {
/// used for accessibility. /// used for accessibility.
final String tooltip; final String tooltip;
/// The default icon and text color.
///
/// Defaults to [ThemeData.accentIconTheme.color] for the current theme.
final Color foregroundColor;
/// The color to use when filling the button. /// The color to use when filling the button.
/// ///
/// Defaults to the accent color of the current theme. /// Defaults to [ThemeData.accentColor] for the current theme.
final Color backgroundColor; final Color backgroundColor;
/// The tag to apply to the button's [Hero] widget. /// The tag to apply to the button's [Hero] widget.
...@@ -141,13 +204,32 @@ class FloatingActionButton extends StatefulWidget { ...@@ -141,13 +204,32 @@ class FloatingActionButton extends StatefulWidget {
/// floating action button. /// floating action button.
final double notchMargin; final double notchMargin;
/// The shape of the button's [Material].
///
/// The button's highlight and splash are clipped to this shape. If the
/// button has an elevation, then its drop shadow is defined by this
/// shape as well.
final ShapeBorder shape;
/// True if this is an "extended" floating action button.
///
/// Typically [extended] buttons have a [StadiumBorder] [shape]
/// and have been created with the [FloatingActionButton.extended]
/// constructor.
///
/// The [Scaffold] animates the appearance of ordinary floating
/// action buttons with scale and rotation transitions. Extended
/// floating action buttons are scaled and faded in.
final bool isExtended;
final BoxConstraints _sizeConstraints;
@override @override
_FloatingActionButtonState createState() => new _FloatingActionButtonState(); _FloatingActionButtonState createState() => new _FloatingActionButtonState();
} }
class _FloatingActionButtonState extends State<FloatingActionButton> { class _FloatingActionButtonState extends State<FloatingActionButton> {
bool _highlight = false; bool _highlight = false;
VoidCallback _clearComputeNotch; VoidCallback _clearComputeNotch;
void _handleHighlightChanged(bool value) { void _handleHighlightChanged(bool value) {
...@@ -158,25 +240,33 @@ class _FloatingActionButtonState extends State<FloatingActionButton> { ...@@ -158,25 +240,33 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color iconColor = Colors.white; final ThemeData theme = Theme.of(context);
Color materialColor = widget.backgroundColor; final Color foregroundColor = widget.foregroundColor ?? theme.accentIconTheme.color;
if (materialColor == null) {
final ThemeData themeData = Theme.of(context);
materialColor = themeData.accentColor;
iconColor = themeData.accentIconTheme.color;
}
Widget result; Widget result;
if (widget.child != null) { if (widget.child != null) {
result = new Center( result = IconTheme.merge(
child: IconTheme.merge( data: new IconThemeData(
data: new IconThemeData(color: iconColor), color: foregroundColor,
child: widget.child,
), ),
child: widget.child,
); );
} }
result = new RawMaterialButton(
onPressed: widget.onPressed,
onHighlightChanged: _handleHighlightChanged,
elevation: _highlight ? widget.highlightElevation : widget.elevation,
constraints: widget._sizeConstraints,
fillColor: widget.backgroundColor ?? theme.accentColor,
textStyle: theme.accentTextTheme.button.copyWith(
color: foregroundColor,
letterSpacing: 1.2,
),
shape: widget.shape,
child: result,
);
if (widget.tooltip != null) { if (widget.tooltip != null) {
result = new Tooltip( result = new Tooltip(
message: widget.tooltip, message: widget.tooltip,
...@@ -184,25 +274,6 @@ class _FloatingActionButtonState extends State<FloatingActionButton> { ...@@ -184,25 +274,6 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
); );
} }
result = new Material(
color: materialColor,
type: MaterialType.circle,
elevation: _highlight ? widget.highlightElevation : widget.elevation,
child: new Container(
width: widget.mini ? _kSizeMini : _kSize,
height: widget.mini ? _kSizeMini : _kSize,
child: new Semantics(
button: true,
enabled: widget.onPressed != null,
child: new InkWell(
onTap: widget.onPressed,
onHighlightChanged: _handleHighlightChanged,
child: result,
),
),
),
);
if (widget.heroTag != null) { if (widget.heroTag != null) {
result = new Hero( result = new Hero(
tag: widget.heroTag, tag: widget.heroTag,
......
...@@ -18,6 +18,7 @@ import 'button_theme.dart'; ...@@ -18,6 +18,7 @@ import 'button_theme.dart';
import 'divider.dart'; import 'divider.dart';
import 'drawer.dart'; import 'drawer.dart';
import 'flexible_space_bar.dart'; import 'flexible_space_bar.dart';
import 'floating_action_button.dart';
import 'floating_action_button_location.dart'; import 'floating_action_button_location.dart';
import 'material.dart'; import 'material.dart';
import 'snack_bar.dart'; import 'snack_bar.dart';
...@@ -59,11 +60,11 @@ enum _ScaffoldSlot { ...@@ -59,11 +60,11 @@ enum _ScaffoldSlot {
/// The geometry of the [Scaffold] after all its contents have been laid out /// The geometry of the [Scaffold] after all its contents have been laid out
/// except the [FloatingActionButton]. /// except the [FloatingActionButton].
/// ///
/// The [Scaffold] passes this prelayout geometry to its /// The [Scaffold] passes this prelayout geometry to its
/// [FloatingActionButtonLocation], which produces an [Offset] that the /// [FloatingActionButtonLocation], which produces an [Offset] that the
/// [Scaffold] uses to position the [FloatingActionButton]. /// [Scaffold] uses to position the [FloatingActionButton].
/// ///
/// For a description of the [Scaffold]'s geometry after it has /// For a description of the [Scaffold]'s geometry after it has
/// finished laying out, see the [ScaffoldGeometry]. /// finished laying out, see the [ScaffoldGeometry].
@immutable @immutable
...@@ -71,35 +72,35 @@ class ScaffoldPrelayoutGeometry { ...@@ -71,35 +72,35 @@ class ScaffoldPrelayoutGeometry {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const ScaffoldPrelayoutGeometry({ const ScaffoldPrelayoutGeometry({
@required this.bottomSheetSize, @required this.bottomSheetSize,
@required this.contentBottom, @required this.contentBottom,
@required this.contentTop, @required this.contentTop,
@required this.floatingActionButtonSize, @required this.floatingActionButtonSize,
@required this.minInsets, @required this.minInsets,
@required this.scaffoldSize, @required this.scaffoldSize,
@required this.snackBarSize, @required this.snackBarSize,
@required this.textDirection, @required this.textDirection,
}); });
/// The [Size] of [Scaffold.floatingActionButton]. /// The [Size] of [Scaffold.floatingActionButton].
/// ///
/// If [Scaffold.floatingActionButton] is null, this will be [Size.zero]. /// If [Scaffold.floatingActionButton] is null, this will be [Size.zero].
final Size floatingActionButtonSize; final Size floatingActionButtonSize;
/// The [Size] of the [Scaffold]'s [BottomSheet]. /// The [Size] of the [Scaffold]'s [BottomSheet].
/// ///
/// If the [Scaffold] is not currently showing a [BottomSheet], /// If the [Scaffold] is not currently showing a [BottomSheet],
/// this will be [Size.zero]. /// this will be [Size.zero].
final Size bottomSheetSize; final Size bottomSheetSize;
/// The vertical distance from the Scaffold's origin to the bottom of /// The vertical distance from the Scaffold's origin to the bottom of
/// [Scaffold.body]. /// [Scaffold.body].
/// ///
/// This is useful in a [FloatingActionButtonLocation] designed to /// This is useful in a [FloatingActionButtonLocation] designed to
/// place the [FloatingActionButton] at the bottom of the screen, while /// place the [FloatingActionButton] at the bottom of the screen, while
/// keeping it above the [BottomSheet], the [Scaffold.bottomNavigationBar], /// keeping it above the [BottomSheet], the [Scaffold.bottomNavigationBar],
/// or the keyboard. /// or the keyboard.
/// ///
/// Note that [Scaffold.body] is laid out with respect to [minInsets] already. /// Note that [Scaffold.body] is laid out with respect to [minInsets] already.
/// This means that a [FloatingActionButtonLocation] does not need to factor /// This means that a [FloatingActionButtonLocation] does not need to factor
/// in [minInsets.bottom] when aligning a [FloatingActionButton] to [contentBottom]. /// in [minInsets.bottom] when aligning a [FloatingActionButton] to [contentBottom].
...@@ -107,11 +108,11 @@ class ScaffoldPrelayoutGeometry { ...@@ -107,11 +108,11 @@ class ScaffoldPrelayoutGeometry {
/// The vertical distance from the [Scaffold]'s origin to the top of /// The vertical distance from the [Scaffold]'s origin to the top of
/// [Scaffold.body]. /// [Scaffold.body].
/// ///
/// This is useful in a [FloatingActionButtonLocation] designed to /// This is useful in a [FloatingActionButtonLocation] designed to
/// place the [FloatingActionButton] at the top of the screen, while /// place the [FloatingActionButton] at the top of the screen, while
/// keeping it below the [Scaffold.appBar]. /// keeping it below the [Scaffold.appBar].
/// ///
/// Note that [Scaffold.body] is laid out with respect to [minInsets] already. /// Note that [Scaffold.body] is laid out with respect to [minInsets] already.
/// This means that a [FloatingActionButtonLocation] does not need to factor /// This means that a [FloatingActionButtonLocation] does not need to factor
/// in [minInsets.top] when aligning a [FloatingActionButton] to [contentTop]. /// in [minInsets.top] when aligning a [FloatingActionButton] to [contentTop].
...@@ -119,33 +120,33 @@ class ScaffoldPrelayoutGeometry { ...@@ -119,33 +120,33 @@ class ScaffoldPrelayoutGeometry {
/// The minimum padding to inset the [FloatingActionButton] by for it /// The minimum padding to inset the [FloatingActionButton] by for it
/// to remain visible. /// to remain visible.
/// ///
/// This value is the result of calling [MediaQuery.padding] in the /// This value is the result of calling [MediaQuery.padding] in the
/// [Scaffold]'s [BuildContext], /// [Scaffold]'s [BuildContext],
/// and is useful for insetting the [FloatingActionButton] to avoid features like /// and is useful for insetting the [FloatingActionButton] to avoid features like
/// the system status bar or the keyboard. /// the system status bar or the keyboard.
/// ///
/// If [Scaffold.resizeToAvoidBottomPadding] is set to false, [minInsets.bottom] /// If [Scaffold.resizeToAvoidBottomPadding] is set to false, [minInsets.bottom]
/// will be 0.0 instead of [MediaQuery.padding.bottom]. /// will be 0.0 instead of [MediaQuery.padding.bottom].
final EdgeInsets minInsets; final EdgeInsets minInsets;
/// The [Size] of the whole [Scaffold]. /// The [Size] of the whole [Scaffold].
/// ///
/// If the [Size] of the [Scaffold]'s contents is modified by values such as /// If the [Size] of the [Scaffold]'s contents is modified by values such as
/// [Scaffold.resizeToAvoidBottomPadding] or the keyboard opening, then the /// [Scaffold.resizeToAvoidBottomPadding] or the keyboard opening, then the
/// [scaffoldSize] will not reflect those changes. /// [scaffoldSize] will not reflect those changes.
/// ///
/// This means that [FloatingActionButtonLocation]s designed to reposition /// This means that [FloatingActionButtonLocation]s designed to reposition
/// the [FloatingActionButton] based on events such as the keyboard popping /// the [FloatingActionButton] based on events such as the keyboard popping
/// up should use [minInsets] to make sure that the [FloatingActionButton] is /// up should use [minInsets] to make sure that the [FloatingActionButton] is
/// inset by enough to remain visible. /// inset by enough to remain visible.
/// ///
/// See [minInsets] and [MediaQuery.padding] for more information on the appropriate /// See [minInsets] and [MediaQuery.padding] for more information on the appropriate
/// insets to apply. /// insets to apply.
final Size scaffoldSize; final Size scaffoldSize;
/// The [Size] of the [Scaffold]'s [SnackBar]. /// The [Size] of the [Scaffold]'s [SnackBar].
/// ///
/// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero]. /// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero].
final Size snackBarSize; final Size snackBarSize;
...@@ -159,7 +160,7 @@ class ScaffoldPrelayoutGeometry { ...@@ -159,7 +160,7 @@ class ScaffoldPrelayoutGeometry {
/// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition. /// when a running [FloatingActionButtonLocation] transition is interrupted by a new transition.
@immutable @immutable
class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation { class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation {
const _TransitionSnapshotFabLocation(this.begin, this.end, this.animator, this.progress); const _TransitionSnapshotFabLocation(this.begin, this.end, this.animator, this.progress);
final FloatingActionButtonLocation begin; final FloatingActionButtonLocation begin;
...@@ -170,8 +171,8 @@ class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation { ...@@ -170,8 +171,8 @@ class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation {
@override @override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
return animator.getOffset( return animator.getOffset(
begin: begin.getOffset(scaffoldGeometry), begin: begin.getOffset(scaffoldGeometry),
end: end.getOffset(scaffoldGeometry), end: end.getOffset(scaffoldGeometry),
progress: progress, progress: progress,
); );
} }
...@@ -186,15 +187,15 @@ class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation { ...@@ -186,15 +187,15 @@ class _TransitionSnapshotFabLocation extends FloatingActionButtonLocation {
/// ///
/// To get a [ValueNotifier] for the scaffold geometry of a given /// To get a [ValueNotifier] for the scaffold geometry of a given
/// [BuildContext], use [Scaffold.geometryOf]. /// [BuildContext], use [Scaffold.geometryOf].
/// ///
/// The ScaffoldGeometry is only available during the paint phase, because /// The ScaffoldGeometry is only available during the paint phase, because
/// its value is computed during the animation and layout phases prior to painting. /// its value is computed during the animation and layout phases prior to painting.
/// ///
/// For an example of using the [ScaffoldGeometry], see the [BottomAppBar], /// For an example of using the [ScaffoldGeometry], see the [BottomAppBar],
/// which uses the [ScaffoldGeometry] to paint a notch around the /// which uses the [ScaffoldGeometry] to paint a notch around the
/// [FloatingActionButton]. /// [FloatingActionButton].
/// ///
/// For information about the [Scaffold]'s geometry that is used while laying /// For information about the [Scaffold]'s geometry that is used while laying
/// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry]. /// out the [FloatingActionButton], see [ScaffoldPrelayoutGeometry].
@immutable @immutable
class ScaffoldGeometry { class ScaffoldGeometry {
...@@ -217,7 +218,7 @@ class ScaffoldGeometry { ...@@ -217,7 +218,7 @@ class ScaffoldGeometry {
final Rect floatingActionButtonArea; final Rect floatingActionButtonArea;
/// A [ComputeNotch] for the floating action button. /// A [ComputeNotch] for the floating action button.
/// ///
/// The contract for this [ComputeNotch] is described in [ComputeNotch] and /// The contract for this [ComputeNotch] is described in [ComputeNotch] and
/// [Scaffold.setFloatingActionButtonNotchFor]. /// [Scaffold.setFloatingActionButtonNotchFor].
final ComputeNotch floatingActionButtonNotch; final ComputeNotch floatingActionButtonNotch;
...@@ -328,7 +329,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl ...@@ -328,7 +329,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
class _ScaffoldLayout extends MultiChildLayoutDelegate { class _ScaffoldLayout extends MultiChildLayoutDelegate {
_ScaffoldLayout({ _ScaffoldLayout({
@required this.minInsets, @required this.minInsets,
@required this.textDirection, @required this.textDirection,
@required this.geometryNotifier, @required this.geometryNotifier,
// for floating action button // for floating action button
...@@ -336,7 +337,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -336,7 +337,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@required this.currentFloatingActionButtonLocation, @required this.currentFloatingActionButtonLocation,
@required this.floatingActionButtonMoveAnimationProgress, @required this.floatingActionButtonMoveAnimationProgress,
@required this.floatingActionButtonMotionAnimator, @required this.floatingActionButtonMotionAnimator,
}) : assert(previousFloatingActionButtonLocation != null), }) : assert(previousFloatingActionButtonLocation != null),
assert(currentFloatingActionButtonLocation != null); assert(currentFloatingActionButtonLocation != null);
final EdgeInsets minInsets; final EdgeInsets minInsets;
...@@ -431,7 +432,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -431,7 +432,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
Rect floatingActionButtonRect; Rect floatingActionButtonRect;
if (hasChild(_ScaffoldSlot.floatingActionButton)) { if (hasChild(_ScaffoldSlot.floatingActionButton)) {
final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints); final Size fabSize = layoutChild(_ScaffoldSlot.floatingActionButton, looseConstraints);
// To account for the FAB position being changed, we'll animate between // To account for the FAB position being changed, we'll animate between
// the old and new positions. // the old and new positions.
final ScaffoldPrelayoutGeometry currentGeometry = new ScaffoldPrelayoutGeometry( final ScaffoldPrelayoutGeometry currentGeometry = new ScaffoldPrelayoutGeometry(
...@@ -447,8 +448,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -447,8 +448,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry); final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry);
final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry); final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry);
final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset( final Offset fabOffset = floatingActionButtonMotionAnimator.getOffset(
begin: previousFabOffset, begin: previousFabOffset,
end: currentFabOffset, end: currentFabOffset,
progress: floatingActionButtonMoveAnimationProgress, progress: floatingActionButtonMoveAnimationProgress,
); );
positionChild(_ScaffoldSlot.floatingActionButton, fabOffset); positionChild(_ScaffoldSlot.floatingActionButton, fabOffset);
...@@ -489,7 +490,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -489,7 +490,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
/// Handler for scale and rotation animations in the [FloatingActionButton]. /// Handler for scale and rotation animations in the [FloatingActionButton].
/// ///
/// Currently, there are two types of [FloatingActionButton] animations: /// Currently, there are two types of [FloatingActionButton] animations:
/// ///
/// * Entrance/Exit animations, which this widget triggers /// * Entrance/Exit animations, which this widget triggers
/// when the [FloatingActionButton] is added, updated, or removed. /// when the [FloatingActionButton] is added, updated, or removed.
/// * Motion animations, which are triggered by the [Scaffold] /// * Motion animations, which are triggered by the [Scaffold]
...@@ -501,7 +502,7 @@ class _FloatingActionButtonTransition extends StatefulWidget { ...@@ -501,7 +502,7 @@ class _FloatingActionButtonTransition extends StatefulWidget {
@required this.fabMoveAnimation, @required this.fabMoveAnimation,
@required this.fabMotionAnimator, @required this.fabMotionAnimator,
@required this.geometryNotifier, @required this.geometryNotifier,
}) : assert(fabMoveAnimation != null), }) : assert(fabMoveAnimation != null),
assert(fabMotionAnimator != null), assert(fabMotionAnimator != null),
super(key: key); super(key: key);
...@@ -524,6 +525,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -524,6 +525,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
AnimationController _currentController; AnimationController _currentController;
// The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations. // The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations.
Animation<double> _currentScaleAnimation; Animation<double> _currentScaleAnimation;
Animation<double> _extendedCurrentScaleAnimation;
Animation<double> _currentRotationAnimation; Animation<double> _currentRotationAnimation;
Widget _previousChild; Widget _previousChild;
...@@ -535,7 +537,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -535,7 +537,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
duration: kFloatingActionButtonSegue, duration: kFloatingActionButtonSegue,
vsync: this, vsync: this,
)..addStatusListener(_handlePreviousAnimationStatusChanged); )..addStatusListener(_handlePreviousAnimationStatusChanged);
_currentController = new AnimationController( _currentController = new AnimationController(
duration: kFloatingActionButtonSegue, duration: kFloatingActionButtonSegue,
vsync: this, vsync: this,
...@@ -601,7 +603,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -601,7 +603,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
curve: Curves.easeIn, curve: Curves.easeIn,
); );
final Animation<double> previousExitRotationAnimation = new Tween<double>(begin: 1.0, end: 1.0).animate( final Animation<double> previousExitRotationAnimation = new Tween<double>(begin: 1.0, end: 1.0).animate(
new CurvedAnimation(parent: _previousController, curve: Curves.easeIn), new CurvedAnimation(
parent: _previousController,
curve: Curves.easeIn,
),
); );
final CurvedAnimation currentEntranceScaleAnimation = new CurvedAnimation( final CurvedAnimation currentEntranceScaleAnimation = new CurvedAnimation(
...@@ -609,19 +614,26 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -609,19 +614,26 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
curve: Curves.easeIn, curve: Curves.easeIn,
); );
final Animation<double> currentEntranceRotationAnimation = new Tween<double>( final Animation<double> currentEntranceRotationAnimation = new Tween<double>(
begin: 1.0 - kFloatingActionButtonTurnInterval, begin: 1.0 - kFloatingActionButtonTurnInterval,
end: 1.0, end: 1.0,
).animate( ).animate(
new CurvedAnimation(parent: _currentController, curve: Curves.easeIn), new CurvedAnimation(
parent: _currentController,
curve: Curves.easeIn
),
); );
// Get the animations for when the FAB is moving. // Get the animations for when the FAB is moving.
final Animation<double> moveScaleAnimation = widget.fabMotionAnimator.getScaleAnimation(parent: widget.fabMoveAnimation); final Animation<double> moveScaleAnimation = widget.fabMotionAnimator.getScaleAnimation(parent: widget.fabMoveAnimation);
final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation); final Animation<double> moveRotationAnimation = widget.fabMotionAnimator.getRotationAnimation(parent: widget.fabMoveAnimation);
// Aggregate the animations. // Aggregate the animations.
_previousScaleAnimation = new AnimationMin<double>(moveScaleAnimation, previousExitScaleAnimation); _previousScaleAnimation = new AnimationMin<double>(moveScaleAnimation, previousExitScaleAnimation);
_currentScaleAnimation = new AnimationMin<double>(moveScaleAnimation, currentEntranceScaleAnimation); _currentScaleAnimation = new AnimationMin<double>(moveScaleAnimation, currentEntranceScaleAnimation);
_extendedCurrentScaleAnimation = new CurvedAnimation(
parent: _currentScaleAnimation,
curve: const Interval(0.0, 0.1),
);
_previousRotationAnimation = new TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation); _previousRotationAnimation = new TrainHoppingAnimation(previousExitRotationAnimation, moveRotationAnimation);
_currentRotationAnimation = new TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation); _currentRotationAnimation = new TrainHoppingAnimation(currentEntranceRotationAnimation, moveRotationAnimation);
...@@ -640,26 +652,56 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -640,26 +652,56 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
}); });
} }
bool _isExtendedFloatingActionButton(Widget widget) {
if (widget is! FloatingActionButton)
return false;
final FloatingActionButton fab = widget;
return fab.isExtended;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> children = <Widget>[]; final List<Widget> children = <Widget>[];
if (_previousController.status != AnimationStatus.dismissed) { if (_previousController.status != AnimationStatus.dismissed) {
if (_isExtendedFloatingActionButton(_previousChild)) {
children.add(new FadeTransition(
opacity: _previousScaleAnimation,
child: _previousChild,
));
} else {
children.add(new ScaleTransition(
scale: _previousScaleAnimation,
child: new RotationTransition(
turns: _previousRotationAnimation,
child: _previousChild,
),
));
}
}
if (_isExtendedFloatingActionButton(widget.child)) {
children.add(new ScaleTransition( children.add(new ScaleTransition(
scale: _previousScaleAnimation, scale: _extendedCurrentScaleAnimation,
child: new FadeTransition(
opacity: _currentScaleAnimation,
child: widget.child,
),
));
} else {
children.add(new ScaleTransition(
scale: _currentScaleAnimation,
child: new RotationTransition( child: new RotationTransition(
turns: _previousRotationAnimation, turns: _currentRotationAnimation,
child: _previousChild, child: widget.child,
), ),
)); ));
} }
children.add(new ScaleTransition(
scale: _currentScaleAnimation, return new Stack(
child: new RotationTransition( alignment: Alignment.centerRight,
turns: _currentRotationAnimation, children: children,
child: widget.child, );
),
));
return new Stack(children: children);
} }
void _onProgressChanged() { void _onProgressChanged() {
...@@ -689,10 +731,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -689,10 +731,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// of an app using the [bottomNavigationBar] property. /// of an app using the [bottomNavigationBar] property.
/// * [FloatingActionButton], which is a circular button typically shown in the /// * [FloatingActionButton], which is a circular button typically shown in the
/// bottom right corner of the app using the [floatingActionButton] property. /// bottom right corner of the app using the [floatingActionButton] property.
/// * [FloatingActionButtonLocation], which is used to place the /// * [FloatingActionButtonLocation], which is used to place the
/// [floatingActionButton] within the [Scaffold]'s layout. /// [floatingActionButton] within the [Scaffold]'s layout.
/// * [FloatingActionButtonAnimator], which is used to animate the /// * [FloatingActionButtonAnimator], which is used to animate the
/// [floatingActionButton] from one [floatingActionButtonLocation] to /// [floatingActionButton] from one [floatingActionButtonLocation] to
/// another. /// another.
/// * [Drawer], which is a vertical panel that is typically displayed to the /// * [Drawer], which is a vertical panel that is typically displayed to the
/// left of the body (and often hidden on phones) using the [drawer] /// left of the body (and often hidden on phones) using the [drawer]
...@@ -753,12 +795,12 @@ class Scaffold extends StatefulWidget { ...@@ -753,12 +795,12 @@ class Scaffold extends StatefulWidget {
final Widget floatingActionButton; final Widget floatingActionButton;
/// Responsible for determining where the [floatingActionButton] should go. /// Responsible for determining where the [floatingActionButton] should go.
/// ///
/// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat]. /// If null, the [ScaffoldState] will use the default location, [FloatingActionButtonLocation.endFloat].
final FloatingActionButtonLocation floatingActionButtonLocation; final FloatingActionButtonLocation floatingActionButtonLocation;
/// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation]. /// Animator to move the [floatingActionButton] to a new [floatingActionButtonLocation].
/// ///
/// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling]. /// If null, the [ScaffoldState] will use the default animator, [FloatingActionButtonAnimator.scaling].
final FloatingActionButtonAnimator floatingActionButtonAnimator; final FloatingActionButtonAnimator floatingActionButtonAnimator;
...@@ -1307,10 +1349,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1307,10 +1349,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator; _floatingActionButtonAnimator = widget.floatingActionButtonAnimator ?? _kDefaultFloatingActionButtonAnimator;
_previousFloatingActionButtonLocation = _floatingActionButtonLocation; _previousFloatingActionButtonLocation = _floatingActionButtonLocation;
_floatingActionButtonMoveController = new AnimationController( _floatingActionButtonMoveController = new AnimationController(
vsync: this, vsync: this,
lowerBound: 0.0, lowerBound: 0.0,
upperBound: 1.0, upperBound: 1.0,
value: 1.0, value: 1.0,
duration: kFloatingActionButtonSegue * 2, duration: kFloatingActionButtonSegue * 2,
); );
} }
...@@ -1567,7 +1609,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1567,7 +1609,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final EdgeInsets minInsets = mediaQuery.padding.copyWith( final EdgeInsets minInsets = mediaQuery.padding.copyWith(
bottom: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0, bottom: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0,
); );
return new _ScaffoldScope( return new _ScaffoldScope(
hasDrawer: hasDrawer, hasDrawer: hasDrawer,
geometryNotifier: _geometryNotifier, geometryNotifier: _geometryNotifier,
......
...@@ -65,10 +65,59 @@ void main() { ...@@ -65,10 +65,59 @@ void main() {
expect(find.byType(Text), findsNothing); expect(find.byType(Text), findsNothing);
await tester.longPress(find.byType(FloatingActionButton)); await tester.longPress(find.byType(FloatingActionButton));
await tester.pump(); await tester.pumpAndSettle();
expect(find.byType(Text), findsOneWidget); expect(find.byType(Text), findsOneWidget);
}); });
testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: const Scaffold(
floatingActionButton: const FloatingActionButton(onPressed: null),
),
),
);
final Finder fabFinder = find.byType(FloatingActionButton);
FloatingActionButton getFabWidget() {
return tester.widget<FloatingActionButton>(fabFinder);
}
expect(getFabWidget().isExtended, false);
expect(getFabWidget().shape, const CircleBorder());
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
floatingActionButton: new FloatingActionButton.extended(
label: const Text('label'),
icon: const Icon(Icons.android),
onPressed: null,
),
),
),
);
expect(getFabWidget().isExtended, true);
expect(getFabWidget().shape, const StadiumBorder());
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
// Verify that the widget's height is 48 and that its internal
/// horizontal layout is: 16 icon 8 label 20
expect(tester.getSize(fabFinder).height, 48.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
final double iconLeft = tester.getTopLeft(find.byType(Icon)).dx;
final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
final double labelLeft = tester.getTopLeft(find.text('label')).dx;
final double labelRight = tester.getTopRight(find.text('label')).dx;
expect(iconLeft - fabLeft, 16.0);
expect(labelLeft - iconRight, 8.0);
expect(fabRight - labelRight, 20.0);
});
testWidgets('Floating Action Button heroTag', (WidgetTester tester) async { testWidgets('Floating Action Button heroTag', (WidgetTester tester) async {
BuildContext theContext; BuildContext theContext;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -372,7 +421,7 @@ class GeometryListenerState extends State<GeometryListener> { ...@@ -372,7 +421,7 @@ class GeometryListenerState extends State<GeometryListener> {
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context); final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable) if (geometryListenable == newListenable)
return; return;
geometryListenable = newListenable; geometryListenable = newListenable;
cache = new GeometryCachePainter(geometryListenable); cache = new GeometryCachePainter(geometryListenable);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment