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(),
)
), ),
floatingActionButton: !_selectedPage.fabDefined ? null : new FloatingActionButton( actions: <Widget>[
key: _selectedPage.fabKey, new IconButton(
tooltip: 'Show explanation', icon: const Icon(Icons.sentiment_very_satisfied),
backgroundColor: _selectedPage.fabColor, onPressed: () {
child: _selectedPage.fabIcon, setState(() {
onPressed: _showExplanatoryText _extendedButtons = !_extendedButtons;
});
},
),
],
), ),
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';
...@@ -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;
...@@ -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(
...@@ -612,7 +617,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -612,7 +617,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
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.
...@@ -622,6 +630,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -622,6 +630,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
// 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,10 +652,24 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -640,10 +652,24 @@ 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( children.add(new ScaleTransition(
scale: _previousScaleAnimation, scale: _previousScaleAnimation,
child: new RotationTransition( child: new RotationTransition(
...@@ -652,6 +678,17 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -652,6 +678,17 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
), ),
)); ));
} }
}
if (_isExtendedFloatingActionButton(widget.child)) {
children.add(new ScaleTransition(
scale: _extendedCurrentScaleAnimation,
child: new FadeTransition(
opacity: _currentScaleAnimation,
child: widget.child,
),
));
} else {
children.add(new ScaleTransition( children.add(new ScaleTransition(
scale: _currentScaleAnimation, scale: _currentScaleAnimation,
child: new RotationTransition( child: new RotationTransition(
...@@ -659,7 +696,12 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -659,7 +696,12 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
child: widget.child, child: widget.child,
), ),
)); ));
return new Stack(children: children); }
return new Stack(
alignment: Alignment.centerRight,
children: children,
);
} }
void _onProgressChanged() { void _onProgressChanged() {
......
...@@ -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(
......
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