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
TabController _controller;
_Page _selectedPage;
bool _extendedButtons;
@override
void initState() {
......@@ -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
Widget build(BuildContext context) {
return new Scaffold(
......@@ -110,15 +135,19 @@ class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStat
bottom: new TabBar(
controller: _controller,
tabs: _allPages.map((_Page page) => new Tab(text: page.label.toUpperCase())).toList(),
)
),
floatingActionButton: !_selectedPage.fabDefined ? null : new FloatingActionButton(
key: _selectedPage.fabKey,
tooltip: 'Show explanation',
backgroundColor: _selectedPage.fabColor,
child: _selectedPage.fabIcon,
onPressed: _showExplanatoryText
),
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.sentiment_very_satisfied),
onPressed: () {
setState(() {
_extendedButtons = !_extendedButtons;
});
},
),
],
),
floatingActionButton: buildFloatingActionButton(_selectedPage),
body: new TabBarView(
controller: _controller,
children: _allPages.map(buildTabView).toList()
......
......@@ -5,19 +5,28 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'ink_well.dart';
import 'material.dart';
import 'button.dart';
import 'scaffold.dart';
import 'theme.dart';
import 'tooltip.dart';
// TODO(eseidel): This needs to change based on device size?
// http://material.google.com/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
const double _kSize = 56.0;
const double _kSizeMini = 40.0;
const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor(
width: 56.0,
height: 56.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 {
const _DefaultHeroTag();
......@@ -52,13 +61,15 @@ class _DefaultHeroTag {
/// * [FlatButton]
/// * <https://material.google.com/components/buttons-floating-action-button.html>
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({
Key key,
this.child,
this.tooltip,
this.foregroundColor,
this.backgroundColor,
this.heroTag: const _DefaultHeroTag(),
this.elevation: 6.0,
......@@ -66,7 +77,54 @@ class FloatingActionButton extends StatefulWidget {
@required this.onPressed,
this.mini: false,
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.
///
......@@ -79,9 +137,14 @@ class FloatingActionButton extends StatefulWidget {
/// used for accessibility.
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.
///
/// Defaults to the accent color of the current theme.
/// Defaults to [ThemeData.accentColor] for the current theme.
final Color backgroundColor;
/// The tag to apply to the button's [Hero] widget.
......@@ -141,13 +204,32 @@ class FloatingActionButton extends StatefulWidget {
/// floating action button.
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
_FloatingActionButtonState createState() => new _FloatingActionButtonState();
}
class _FloatingActionButtonState extends State<FloatingActionButton> {
bool _highlight = false;
VoidCallback _clearComputeNotch;
void _handleHighlightChanged(bool value) {
......@@ -158,25 +240,33 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
@override
Widget build(BuildContext context) {
Color iconColor = Colors.white;
Color materialColor = widget.backgroundColor;
if (materialColor == null) {
final ThemeData themeData = Theme.of(context);
materialColor = themeData.accentColor;
iconColor = themeData.accentIconTheme.color;
}
final ThemeData theme = Theme.of(context);
final Color foregroundColor = widget.foregroundColor ?? theme.accentIconTheme.color;
Widget result;
if (widget.child != null) {
result = new Center(
child: IconTheme.merge(
data: new IconThemeData(color: iconColor),
child: widget.child,
result = IconTheme.merge(
data: new IconThemeData(
color: foregroundColor,
),
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) {
result = new Tooltip(
message: widget.tooltip,
......@@ -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) {
result = new Hero(
tag: widget.heroTag,
......
......@@ -65,10 +65,59 @@ void main() {
expect(find.byType(Text), findsNothing);
await tester.longPress(find.byType(FloatingActionButton));
await tester.pump();
await tester.pumpAndSettle();
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 {
BuildContext theContext;
await tester.pumpWidget(
......@@ -372,7 +421,7 @@ class GeometryListenerState extends State<GeometryListener> {
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
geometryListenable = newListenable;
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