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,
......
...@@ -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