diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 8ccc9d20e1..86c250e525 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -56,6 +56,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/floating_action_button_theme.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.dart b/packages/flutter/lib/src/material/floating_action_button.dart index 4367980228..a0ee70c654 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'button.dart'; +import 'floating_action_button_theme.dart'; import 'scaffold.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -114,10 +115,9 @@ class _DefaultHeroTag { class FloatingActionButton extends StatelessWidget { /// Creates a circular floating action button. /// - /// The [elevation], [highlightElevation], [mini], [shape], and [clipBehavior] - /// arguments must not be null. Additionally, [elevation], - /// [highlightElevation], and [disabledElevation] (if specified) must be - /// non-negative. + /// The [mini] and [clipBehavior] arguments must be non-null. Additionally, + /// [elevation], [highlightElevation], and [disabledElevation] (if specified) + /// must be non-negative. const FloatingActionButton({ Key key, this.child, @@ -125,56 +125,51 @@ class FloatingActionButton extends StatelessWidget { this.foregroundColor, this.backgroundColor, this.heroTag = const _DefaultHeroTag(), - this.elevation = 6.0, - this.highlightElevation = 12.0, - double disabledElevation, + this.elevation, + this.highlightElevation, + this.disabledElevation, @required this.onPressed, this.mini = false, - this.shape = const CircleBorder(), + this.shape, this.clipBehavior = Clip.none, this.materialTapTargetSize, this.isExtended = false, - }) : assert(elevation != null && elevation >= 0.0), - assert(highlightElevation != null && highlightElevation >= 0.0), + }) : assert(elevation == null || elevation >= 0.0), + assert(highlightElevation == null || highlightElevation >= 0.0), assert(disabledElevation == null || disabledElevation >= 0.0), assert(mini != null), - assert(shape != null), assert(isExtended != null), _sizeConstraints = mini ? _kMiniSizeConstraints : _kSizeConstraints, - disabledElevation = disabledElevation ?? elevation, super(key: key); /// Creates a wider [StadiumBorder]-shaped floating action button with /// an optional [icon] and a [label]. /// - /// The [label], [elevation], [highlightElevation], [clipBehavior] and - /// [shape] arguments must not be null. Additionally, [elevation] - /// [highlightElevation], and [disabledElevation] (if specified) must be - /// non-negative. + /// The [label] and [clipBehavior] arguments must non-null. Additionally, + /// [elevation], [highlightElevation], and [disabledElevation] (if specified) + /// must be non-negative. FloatingActionButton.extended({ Key key, this.tooltip, this.foregroundColor, this.backgroundColor, this.heroTag = const _DefaultHeroTag(), - this.elevation = 6.0, - this.highlightElevation = 12.0, - double disabledElevation, + this.elevation, + this.highlightElevation, + this.disabledElevation, @required this.onPressed, - this.shape = const StadiumBorder(), + this.shape, this.isExtended = true, this.materialTapTargetSize, this.clipBehavior = Clip.none, Widget icon, @required Widget label, - }) : assert(elevation != null && elevation >= 0.0), - assert(highlightElevation != null && highlightElevation >= 0.0), + }) : assert(elevation == null || elevation >= 0.0), + assert(highlightElevation == null || highlightElevation >= 0.0), assert(disabledElevation == null || disabledElevation >= 0.0), - assert(shape != null), assert(isExtended != null), assert(clipBehavior != null), _sizeConstraints = _kExtendedSizeConstraints, - disabledElevation = disabledElevation ?? elevation, mini = false, child = _ChildOverflowBox( child: Row( @@ -319,10 +314,42 @@ class FloatingActionButton extends StatelessWidget { final BoxConstraints _sizeConstraints; + static const double _defaultElevation = 6; + static const double _defaultHighlightElevation = 12; + static const ShapeBorder _defaultShape = CircleBorder(); + static const ShapeBorder _defaultExtendedShape = StadiumBorder(); + @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - final Color foregroundColor = this.foregroundColor ?? theme.accentIconTheme.color; + final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme; + + final Color backgroundColor = this.backgroundColor + ?? floatingActionButtonTheme.backgroundColor + ?? theme.colorScheme.secondary; + final Color foregroundColor = this.foregroundColor + ?? floatingActionButtonTheme.foregroundColor + ?? theme.accentIconTheme.color + ?? theme.colorScheme.onSecondary; + final double elevation = this.elevation + ?? floatingActionButtonTheme.elevation + ?? _defaultElevation; + final double disabledElevation = this.disabledElevation + ?? floatingActionButtonTheme.disabledElevation + ?? elevation; + final double highlightElevation = this.highlightElevation + ?? floatingActionButtonTheme.highlightElevation + ?? _defaultHighlightElevation; + final MaterialTapTargetSize materialTapTargetSize = this.materialTapTargetSize + ?? theme.materialTapTargetSize; + final TextStyle textStyle = theme.accentTextTheme.button.copyWith( + color: foregroundColor, + letterSpacing: 1.2, + ); + final ShapeBorder shape = this.shape + ?? floatingActionButtonTheme.shape + ?? (isExtended ? _defaultExtendedShape : _defaultShape); + Widget result; if (child != null) { @@ -340,12 +367,9 @@ class FloatingActionButton extends StatelessWidget { highlightElevation: highlightElevation, disabledElevation: disabledElevation, constraints: _sizeConstraints, - materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize, - fillColor: backgroundColor ?? theme.accentColor, - textStyle: theme.accentTextTheme.button.copyWith( - color: foregroundColor, - letterSpacing: 1.2, - ), + materialTapTargetSize: materialTapTargetSize, + fillColor: backgroundColor, + textStyle: textStyle, shape: shape, clipBehavior: clipBehavior, child: result, diff --git a/packages/flutter/lib/src/material/floating_action_button_theme.dart b/packages/flutter/lib/src/material/floating_action_button_theme.dart new file mode 100644 index 0000000000..071fff4170 --- /dev/null +++ b/packages/flutter/lib/src/material/floating_action_button_theme.dart @@ -0,0 +1,142 @@ +// Copyright 2019 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:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +/// Defines default property values for descendant [FloatingActionButton] +/// widgets. +/// +/// Descendant widgets obtain the current [FloatingActionButtonThemeData] object +/// using `Theme.of(context).floatingActionButtonTheme`. Instances of +/// [FloatingActionButtonThemeData] can be customized with +/// [FloatingActionButtonThemeData.copyWith]. +/// +/// Typically a [FloatingActionButtonThemeData] is specified as part of the +/// overall [Theme] with [ThemeData.floatingActionButtonTheme]. +/// +/// All [FloatingActionButtonThemeData] properties are `null` by default. +/// When null, the [FloatingActionButton] will use the values from [ThemeData] +/// if they exist, otherwise it will provide its own defaults. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +class FloatingActionButtonThemeData extends Diagnosticable { + /// Creates a theme that can be used for + /// [ThemeData.floatingActionButtonTheme]. + const FloatingActionButtonThemeData({ + this.backgroundColor, + this.foregroundColor, + this.elevation, + this.disabledElevation, + this.highlightElevation, + this.shape, + }); + + /// Color to be used for the unselected, enabled [FloatingActionButton]'s + /// background. + final Color backgroundColor; + + /// Color to be used for the unselected, enabled [FloatingActionButton]'s + /// foreground. + final Color foregroundColor; + + /// The z-coordinate to be used for the unselected, enabled + /// [FloatingActionButton]'s elevation foreground. + final double elevation; + + /// The z-coordinate to be used for the disabled [FloatingActionButton]'s + /// elevation foreground. + final double disabledElevation; + + /// The z-coordinate to be used for the selected, enabled + /// [FloatingActionButton]'s elevation foreground. + final double highlightElevation; + + /// The shape to be used for the floating action button's [Material]. + final ShapeBorder shape; + + /// Creates a copy of this object with the given fields replaced with the + /// new values. + FloatingActionButtonThemeData copyWith({ + Color backgroundColor, + Color foregroundColor, + double elevation, + double disabledElevation, + double highlightElevation, + ShapeBorder shape, + }) { + return FloatingActionButtonThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + elevation: elevation ?? this.elevation, + disabledElevation: disabledElevation ?? this.disabledElevation, + highlightElevation: highlightElevation ?? this.highlightElevation, + shape: shape ?? this.shape, + ); + } + + /// Linearly interpolate between two floating action button themes. + /// + /// If both arguments are null then null is returned. + /// + /// {@macro dart.ui.shadow.lerp} + static FloatingActionButtonThemeData lerp(FloatingActionButtonThemeData a, FloatingActionButtonThemeData b, double t) { + assert(t != null); + if (a == null && b == null) + return null; + return FloatingActionButtonThemeData( + backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), + disabledElevation: lerpDouble(a?.disabledElevation, b?.disabledElevation, t), + highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t), + shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + ); + } + + @override + int get hashCode { + return hashValues( + backgroundColor, + foregroundColor, + elevation, + disabledElevation, + highlightElevation, + shape, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final FloatingActionButtonThemeData otherData = other; + return otherData.backgroundColor == backgroundColor + && otherData.foregroundColor == foregroundColor + && otherData.elevation == elevation + && otherData.disabledElevation == disabledElevation + && otherData.highlightElevation == highlightElevation + && otherData.shape == shape; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + const FloatingActionButtonThemeData defaultData = FloatingActionButtonThemeData(); + + properties.add(DiagnosticsProperty('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor)); + properties.add(DiagnosticsProperty('foregroundColor', foregroundColor, defaultValue: defaultData.foregroundColor)); + properties.add(DiagnosticsProperty('elevation', elevation, defaultValue: defaultData.elevation)); + properties.add(DiagnosticsProperty('disabledElevation', disabledElevation, defaultValue: defaultData.disabledElevation)); + properties.add(DiagnosticsProperty('highlightElevation', highlightElevation, defaultValue: defaultData.highlightElevation)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: defaultData.shape)); + } +} diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 8caa644c04..d1db506b19 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -17,6 +17,7 @@ import 'chip_theme.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'dialog_theme.dart'; +import 'floating_action_button_theme.dart'; import 'ink_splash.dart'; import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'input_decorator.dart'; @@ -159,6 +160,7 @@ class ThemeData extends Diagnosticable { BottomAppBarTheme bottomAppBarTheme, ColorScheme colorScheme, DialogTheme dialogTheme, + FloatingActionButtonThemeData floatingActionButtonTheme, Typography typography, CupertinoThemeData cupertinoOverrideTheme, }) { @@ -257,6 +259,7 @@ class ThemeData extends Diagnosticable { labelStyle: textTheme.body2, ); dialogTheme ??= const DialogTheme(); + floatingActionButtonTheme ??= const FloatingActionButtonThemeData(); cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( @@ -308,6 +311,7 @@ class ThemeData extends Diagnosticable { bottomAppBarTheme: bottomAppBarTheme, colorScheme: colorScheme, dialogTheme: dialogTheme, + floatingActionButtonTheme: floatingActionButtonTheme, typography: typography, cupertinoOverrideTheme: cupertinoOverrideTheme, ); @@ -372,6 +376,7 @@ class ThemeData extends Diagnosticable { @required this.bottomAppBarTheme, @required this.colorScheme, @required this.dialogTheme, + @required this.floatingActionButtonTheme, @required this.typography, @required this.cupertinoOverrideTheme, }) : assert(brightness != null), @@ -421,6 +426,7 @@ class ThemeData extends Diagnosticable { assert(bottomAppBarTheme != null), assert(colorScheme != null), assert(dialogTheme != null), + assert(floatingActionButtonTheme != null), assert(typography != null); // Warning: make sure these properties are in the exact same order as in @@ -662,6 +668,10 @@ class ThemeData extends Diagnosticable { /// A theme for customizing the shape of a dialog. final DialogTheme dialogTheme; + /// A theme for customizing the shape, elevation, and color of a + /// [FloatingActionButton]. + final FloatingActionButtonThemeData floatingActionButtonTheme; + /// The color and geometry [TextTheme] values used to configure [textTheme], /// [primaryTextTheme], and [accentTextTheme]. final Typography typography; @@ -728,6 +738,7 @@ class ThemeData extends Diagnosticable { BottomAppBarTheme bottomAppBarTheme, ColorScheme colorScheme, DialogTheme dialogTheme, + FloatingActionButtonThemeData floatingActionButtonTheme, Typography typography, CupertinoThemeData cupertinoOverrideTheme, }) { @@ -781,6 +792,7 @@ class ThemeData extends Diagnosticable { bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme, colorScheme: colorScheme ?? this.colorScheme, dialogTheme: dialogTheme ?? this.dialogTheme, + floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme, typography: typography ?? this.typography, cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme, ); @@ -912,6 +924,7 @@ class ThemeData extends Diagnosticable { bottomAppBarTheme: BottomAppBarTheme.lerp(a.bottomAppBarTheme, b.bottomAppBarTheme, t), colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t), dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t), + floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t), typography: Typography.lerp(a.typography, b.typography, t), cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme, ); @@ -973,6 +986,7 @@ class ThemeData extends Diagnosticable { (otherData.bottomAppBarTheme == bottomAppBarTheme) && (otherData.colorScheme == colorScheme) && (otherData.dialogTheme == dialogTheme) && + (otherData.floatingActionButtonTheme == floatingActionButtonTheme) && (otherData.typography == typography) && (otherData.cupertinoOverrideTheme == cupertinoOverrideTheme); } @@ -1034,6 +1048,7 @@ class ThemeData extends Diagnosticable { bottomAppBarTheme, colorScheme, dialogTheme, + floatingActionButtonTheme, typography, cupertinoOverrideTheme, ), @@ -1090,6 +1105,7 @@ class ThemeData extends Diagnosticable { properties.add(DiagnosticsProperty('bottomAppBarTheme', bottomAppBarTheme, defaultValue: defaultData.bottomAppBarTheme)); properties.add(DiagnosticsProperty('colorScheme', colorScheme, defaultValue: defaultData.colorScheme)); properties.add(DiagnosticsProperty('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme)); + properties.add(DiagnosticsProperty('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme)); properties.add(DiagnosticsProperty('typography', typography, defaultValue: defaultData.typography)); properties.add(DiagnosticsProperty('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme)); } diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 15b7ddb7b2..b72eff8172 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -110,11 +110,6 @@ void main() { expect(find.text('Add'), findsOneWidget); }); - testWidgets('Floating Action Button elevation when highlighted - defaults', (WidgetTester tester) async { - expect(const FloatingActionButton(onPressed: null).highlightElevation, 12.0); - expect(const FloatingActionButton(onPressed: null, highlightElevation: 0.0).highlightElevation, 0.0); - }); - testWidgets('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -153,9 +148,33 @@ void main() { }); testWidgets('Floating Action Button elevation when disabled - defaults', (WidgetTester tester) async { - expect(FloatingActionButton(onPressed: () { }).disabledElevation, 6.0); - expect(const FloatingActionButton(onPressed: null).disabledElevation, 6.0); - expect(FloatingActionButton(onPressed: () { }, disabledElevation: 0.0).disabledElevation, 0.0); + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: null, + ), + ), + ), + ); + + // Disabled elevation defaults to regular default elevation. + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); + }); + + testWidgets('Floating Action Button elevation when disabled - override', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: null, + disabledElevation: 0, + ), + ), + ), + ); + + expect(tester.widget(find.byType(PhysicalShape)).elevation, 0.0); }); testWidgets('Floating Action Button elevation when disabled - effect', (WidgetTester tester) async { @@ -294,8 +313,14 @@ void main() { return tester.widget(fabFinder); } + final Finder materialButtonFinder = find.byType(RawMaterialButton); + + RawMaterialButton getRawMaterialButtonWidget() { + return tester.widget(materialButtonFinder); + } + expect(getFabWidget().isExtended, false); - expect(getFabWidget().shape, const CircleBorder()); + expect(getRawMaterialButtonWidget().shape, const CircleBorder()); await tester.pumpWidget( MaterialApp( @@ -313,7 +338,7 @@ void main() { ); expect(getFabWidget().isExtended, true); - expect(getFabWidget().shape, const StadiumBorder()); + expect(getRawMaterialButtonWidget().shape, const StadiumBorder()); expect(find.text('label'), findsOneWidget); expect(find.byType(Icon), findsOneWidget); @@ -345,6 +370,12 @@ void main() { return tester.widget(fabFinder); } + final Finder materialButtonFinder = find.byType(RawMaterialButton); + + RawMaterialButton getRawMaterialButtonWidget() { + return tester.widget(materialButtonFinder); + } + await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -360,7 +391,7 @@ void main() { ); expect(getFabWidget().isExtended, true); - expect(getFabWidget().shape, const StadiumBorder()); + expect(getRawMaterialButtonWidget().shape, const StadiumBorder()); expect(find.text('label'), findsOneWidget); expect(find.byType(Icon), findsNothing); diff --git a/packages/flutter/test/material/floating_action_button_theme_test.dart b/packages/flutter/test/material/floating_action_button_theme_test.dart new file mode 100644 index 0000000000..5459ad4172 --- /dev/null +++ b/packages/flutter/test/material/floating_action_button_theme_test.dart @@ -0,0 +1,200 @@ +// Copyright 2019 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'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('FloatingActionButtonThemeData copyWith, ==, hashCode basics', () { + expect(const FloatingActionButtonThemeData(), const FloatingActionButtonThemeData().copyWith()); + expect(const FloatingActionButtonThemeData().hashCode, const FloatingActionButtonThemeData().copyWith().hashCode); + }); + + testWidgets('Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add), + ), + ), + )); + + // The color scheme values are guaranteed to be non null since the default + // [ThemeData] creates it with [ColorScheme.fromSwatch]. + expect(_getRawMaterialButton(tester).fillColor, ThemeData().colorScheme.secondary); + expect(_getRichText(tester).text.style.color, ThemeData().colorScheme.onSecondary); + + // These defaults come directly from the [FloatingActionButton]. + expect(_getRawMaterialButton(tester).elevation, 6); + expect(_getRawMaterialButton(tester).highlightElevation, 12); + expect(_getRawMaterialButton(tester).shape, const CircleBorder()); + }); + + testWidgets('FloatingActionButtonThemeData values are used when no FloatingActionButton properties are specified', (WidgetTester tester) async { + const Color backgroundColor = Color(0xBEEFBEEF); + const Color foregroundColor = Color(0xFACEFACE); + const double elevation = 7; + const double disabledElevation = 1; + const double highlightElevation = 13; + const ShapeBorder shape = StadiumBorder(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData().copyWith( + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + elevation: elevation, + disabledElevation: disabledElevation, + highlightElevation: highlightElevation, + shape: shape, + ) + ), + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add), + ), + ), + )); + + expect(_getRawMaterialButton(tester).fillColor, backgroundColor); + expect(_getRichText(tester).text.style.color, foregroundColor); + expect(_getRawMaterialButton(tester).elevation, elevation); + expect(_getRawMaterialButton(tester).disabledElevation, disabledElevation); + expect(_getRawMaterialButton(tester).highlightElevation, highlightElevation); + expect(_getRawMaterialButton(tester).shape, shape); + }); + + testWidgets('FloatingActionButton values take priority over FloatingActionButtonThemeData values when both properties are specified', (WidgetTester tester) async { + const Color backgroundColor = Color(0xBEEFBEEF); + const Color foregroundColor = Color(0xFACEFACE); + const double elevation = 7; + const double disabledElevation = 1; + const double highlightElevation = 13; + const ShapeBorder shape = StadiumBorder(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData().copyWith( + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Color(0xCAFECAFE), + foregroundColor: Color(0xFEEDFEED), + elevation: 23, + disabledElevation: 11, + highlightElevation: 43, + shape: BeveledRectangleBorder(), + ), + ), + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add), + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + elevation: elevation, + disabledElevation: disabledElevation, + highlightElevation: highlightElevation, + shape: shape, + ), + ), + )); + + expect(_getRawMaterialButton(tester).fillColor, backgroundColor); + expect(_getRichText(tester).text.style.color, foregroundColor); + expect(_getRawMaterialButton(tester).elevation, elevation); + expect(_getRawMaterialButton(tester).disabledElevation, disabledElevation); + expect(_getRawMaterialButton(tester).highlightElevation, highlightElevation); + expect(_getRawMaterialButton(tester).shape, shape); + }); + + testWidgets('FloatingActionButton foreground color uses iconAccentTheme if no widget or widget theme color is specified', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + floatingActionButton: Theme( + data: ThemeData().copyWith( + accentIconTheme: const IconThemeData(color: Color(0xFACEFACE)), + ), + child: FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add), + ), + ), + ), + )); + + expect(_getRichText(tester).text.style.color, const Color(0xFACEFACE)); + }); + + testWidgets('FloatingActionButton uses a custom shape when specified in the theme', (WidgetTester tester) async { + const ShapeBorder customShape = BeveledRectangleBorder(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + shape: customShape, + ), + ), + )); + + expect(_getRawMaterialButton(tester).shape, customShape); + }); + + testWidgets('default FloatingActionButton debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const FloatingActionButtonThemeData ().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('Material implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const FloatingActionButtonThemeData( + backgroundColor: Color(0xCAFECAFE), + foregroundColor: Color(0xFEEDFEED), + elevation: 23, + disabledElevation: 11, + highlightElevation: 43, + shape: BeveledRectangleBorder(), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'backgroundColor: Color(0xcafecafe)', + 'foregroundColor: Color(0xfeedfeed)', + 'elevation: 23.0', + 'disabledElevation: 11.0', + 'highlightElevation: 43.0', + 'shape: BeveledRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)', + ]); + }); +} + +RawMaterialButton _getRawMaterialButton(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(FloatingActionButton), + matching: find.byType(RawMaterialButton), + ), + ); +} + +RichText _getRichText(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(FloatingActionButton), + matching: find.byType(RichText), + ), + ); +}