Migrate FloatingActionButton to Material 3 (#94486)
diff --git a/dev/tools/gen_defaults/data/material-tokens.json b/dev/tools/gen_defaults/data/material-tokens.json
index 3412597..2cd2616 100644
--- a/dev/tools/gen_defaults/data/material-tokens.json
+++ b/dev/tools/gen_defaults/data/material-tokens.json
@@ -1,6 +1,6 @@
{
- "version": "v0.72",
- "date": "2021-12-16 00:27:25.239571",
+ "version": "v0.74",
+ "date": "2022-01-06",
"md.sys.color.light.on-tertiary": "md.ref.palette.tertiary100",
"md.sys.color.light.on-secondary-container": "md.ref.palette.secondary10",
"md.sys.color.light.on-secondary": "md.ref.palette.secondary100",
diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart
index 0b7fefd..fcb6c32 100644
--- a/packages/flutter/lib/src/material/floating_action_button.dart
+++ b/packages/flutter/lib/src/material/floating_action_button.dart
@@ -9,31 +9,14 @@
import 'package:flutter/widgets.dart';
import 'button.dart';
+import 'color_scheme.dart';
import 'floating_action_button_theme.dart';
import 'scaffold.dart';
+import 'text_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'tooltip.dart';
-const BoxConstraints _kSizeConstraints = BoxConstraints.tightFor(
- width: 56.0,
- height: 56.0,
-);
-
-const BoxConstraints _kMiniSizeConstraints = BoxConstraints.tightFor(
- width: 40.0,
- height: 40.0,
-);
-
-const BoxConstraints _kLargeSizeConstraints = BoxConstraints.tightFor(
- width: 96.0,
- height: 96.0,
-);
-
-const BoxConstraints _kExtendedSizeConstraints = BoxConstraints.tightFor(
- height: 48.0,
-);
-
class _DefaultHeroTag {
const _DefaultHeroTag();
@override
@@ -508,82 +491,80 @@
final Widget? _extendedLabel;
- static const double _defaultElevation = 6;
- static const double _defaultFocusElevation = 6;
- static const double _defaultHoverElevation = 8;
- 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 FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme;
+ final FloatingActionButtonThemeData defaults = theme.useMaterial3
+ ? _M3Defaults(context, _floatingActionButtonType, child != null)
+ : _M2Defaults(context, _floatingActionButtonType, child != null);
final Color foregroundColor = this.foregroundColor
?? floatingActionButtonTheme.foregroundColor
- ?? theme.colorScheme.onSecondary;
+ ?? defaults.foregroundColor!;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
- ?? theme.colorScheme.secondary;
+ ?? defaults.backgroundColor!;
final Color focusColor = this.focusColor
?? floatingActionButtonTheme.focusColor
- ?? theme.focusColor;
+ ?? defaults.focusColor!;
final Color hoverColor = this.hoverColor
?? floatingActionButtonTheme.hoverColor
- ?? theme.hoverColor;
+ ?? defaults.hoverColor!;
final Color splashColor = this.splashColor
?? floatingActionButtonTheme.splashColor
- ?? theme.splashColor;
+ ?? defaults.splashColor!;
final double elevation = this.elevation
?? floatingActionButtonTheme.elevation
- ?? _defaultElevation;
+ ?? defaults.elevation!;
final double focusElevation = this.focusElevation
?? floatingActionButtonTheme.focusElevation
- ?? _defaultFocusElevation;
+ ?? defaults.focusElevation!;
final double hoverElevation = this.hoverElevation
?? floatingActionButtonTheme.hoverElevation
- ?? _defaultHoverElevation;
+ ?? defaults.hoverElevation!;
final double disabledElevation = this.disabledElevation
?? floatingActionButtonTheme.disabledElevation
+ ?? defaults.disabledElevation
?? elevation;
final double highlightElevation = this.highlightElevation
?? floatingActionButtonTheme.highlightElevation
- ?? _defaultHighlightElevation;
+ ?? defaults.highlightElevation!;
final MaterialTapTargetSize materialTapTargetSize = this.materialTapTargetSize
?? theme.materialTapTargetSize;
final bool enableFeedback = this.enableFeedback
- ?? floatingActionButtonTheme.enableFeedback ?? true;
+ ?? floatingActionButtonTheme.enableFeedback
+ ?? defaults.enableFeedback!;
+ final double iconSize = floatingActionButtonTheme.iconSize
+ ?? defaults.iconSize!;
final TextStyle extendedTextStyle = (this.extendedTextStyle
- ?? floatingActionButtonTheme.extendedTextStyle
- ?? theme.textTheme.button!.copyWith(letterSpacing: 1.2)).copyWith(color: foregroundColor);
+ ?? floatingActionButtonTheme.extendedTextStyle
+ ?? defaults.extendedTextStyle!).copyWith(color: foregroundColor);
final ShapeBorder shape = this.shape
?? floatingActionButtonTheme.shape
- ?? (isExtended ? _defaultExtendedShape : _defaultShape);
+ ?? defaults.shape!;
BoxConstraints sizeConstraints;
- Widget? resolvedChild = child;
+ Widget? resolvedChild = child != null ? IconTheme.merge(
+ data: IconThemeData(size: iconSize),
+ child: child!,
+ ) : child;
switch(_floatingActionButtonType) {
case _FloatingActionButtonType.regular:
- sizeConstraints = floatingActionButtonTheme.sizeConstraints ?? _kSizeConstraints;
+ sizeConstraints = floatingActionButtonTheme.sizeConstraints ?? defaults.sizeConstraints!;
break;
case _FloatingActionButtonType.small:
- sizeConstraints = floatingActionButtonTheme.smallSizeConstraints ?? _kMiniSizeConstraints;
+ sizeConstraints = floatingActionButtonTheme.smallSizeConstraints ?? defaults.smallSizeConstraints!;
break;
case _FloatingActionButtonType.large:
- sizeConstraints = floatingActionButtonTheme.largeSizeConstraints ?? _kLargeSizeConstraints;
- // The large FAB uses a larger icon.
- resolvedChild = child != null ? IconTheme.merge(
- data: const IconThemeData(size: 36.0),
- child: child!,
- ) : child;
+ sizeConstraints = floatingActionButtonTheme.largeSizeConstraints ?? defaults.largeSizeConstraints!;
break;
case _FloatingActionButtonType.extended:
- sizeConstraints = floatingActionButtonTheme.extendedSizeConstraints ?? _kExtendedSizeConstraints;
+ sizeConstraints = floatingActionButtonTheme.extendedSizeConstraints ?? defaults.extendedSizeConstraints!;
final double iconLabelSpacing = extendedIconLabelSpacing ?? floatingActionButtonTheme.extendedIconLabelSpacing ?? 8.0;
final EdgeInsetsGeometry padding = extendedPadding
?? floatingActionButtonTheme.extendedPadding
- ?? EdgeInsetsDirectional.only(start: child != null && isExtended ? 16.0 : 20.0, end: 20.0);
+ ?? defaults.extendedPadding!;
resolvedChild = _ChildOverflowBox(
child: Padding(
padding: padding,
@@ -730,3 +711,148 @@
}
}
}
+
+// Generate a FloatingActionButtonThemeData that represents
+// the M2 default values. This was generated by hand from the
+// previous hand coded defaults for M2. It uses get method overrides
+// instead of properties to avoid computing values that we may not
+// need upfront.
+class _M2Defaults extends FloatingActionButtonThemeData {
+ _M2Defaults(BuildContext context, this.type, this.hasChild)
+ : _theme = Theme.of(context),
+ _colors = Theme.of(context).colorScheme;
+
+ final _FloatingActionButtonType type;
+ final bool hasChild;
+ final ThemeData _theme;
+ final ColorScheme _colors;
+
+ bool get _isExtended => type == _FloatingActionButtonType.extended;
+ bool get _isLarge => type == _FloatingActionButtonType.large;
+
+ @override Color? get foregroundColor => _colors.onSecondary;
+ @override Color? get backgroundColor => _colors.secondary;
+ @override Color? get focusColor => _theme.focusColor;
+ @override Color? get hoverColor => _theme.hoverColor;
+ @override Color? get splashColor => _theme.splashColor;
+ @override double? get elevation => 6;
+ @override double? get focusElevation => 6;
+ @override double? get hoverElevation => 8;
+ @override double? get highlightElevation => 12;
+ @override ShapeBorder? get shape => _isExtended ? const StadiumBorder() : const CircleBorder();
+ @override bool? get enableFeedback => true;
+ @override double? get iconSize => _isLarge ? 36.0 : 24.0;
+
+ @override
+ BoxConstraints? get sizeConstraints => const BoxConstraints.tightFor(
+ width: 56.0,
+ height: 56.0,
+ );
+
+ @override
+ BoxConstraints? get smallSizeConstraints => const BoxConstraints.tightFor(
+ width: 40.0,
+ height: 40.0,
+ );
+
+ @override
+ BoxConstraints? get largeSizeConstraints => const BoxConstraints.tightFor(
+ width: 96.0,
+ height: 96.0,
+ );
+
+ @override
+ BoxConstraints? get extendedSizeConstraints => const BoxConstraints.tightFor(
+ height: 48.0,
+ );
+
+ @override double? get extendedIconLabelSpacing => 8.0;
+ @override EdgeInsetsGeometry? get extendedPadding => EdgeInsetsDirectional.only(start: hasChild && _isExtended ? 16.0 : 20.0, end: 20.0);
+ @override TextStyle? get extendedTextStyle => _theme.textTheme.button!.copyWith(letterSpacing: 1.2);
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES
+
+// Generated code to the end of this file. Do not edit by hand.
+// These defaults are generated from the Material Design Token
+// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Generated version v0.74, 2022-01-06
+class _M3Defaults extends FloatingActionButtonThemeData {
+ _M3Defaults(this.context, this.type, this.hasChild)
+ : _colors = Theme.of(context).colorScheme,
+ _textTheme = Theme.of(context).textTheme;
+
+ final BuildContext context;
+ final _FloatingActionButtonType type;
+ final bool hasChild;
+ final ColorScheme _colors;
+ final TextTheme _textTheme;
+
+ bool get _isExtended => type == _FloatingActionButtonType.extended;
+
+ @override Color? get foregroundColor => _colors.onPrimaryContainer;
+ @override Color? get backgroundColor => _colors.primaryContainer;
+ @override Color? get splashColor => _colors.onPrimaryContainer.withOpacity(0.12);
+ @override double get elevation => 6.0;
+ @override Color? get focusColor => _colors.onPrimaryContainer.withOpacity(0.12);
+ @override double get focusElevation => 6.0;
+ @override Color? get hoverColor => _colors.onPrimaryContainer.withOpacity(0.08);
+ @override double get hoverElevation => 8.0;
+ @override double get highlightElevation => 6.0;
+
+ @override
+ ShapeBorder? get shape {
+ switch (type) {
+ case _FloatingActionButtonType.regular:
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)));
+ case _FloatingActionButtonType.small:
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)));
+ case _FloatingActionButtonType.large:
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0)));
+ case _FloatingActionButtonType.extended:
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)));
+ }
+ }
+
+ @override bool? get enableFeedback => true;
+
+ @override
+ double? get iconSize {
+ switch (type) {
+ case _FloatingActionButtonType.regular: return 24.0;
+ case _FloatingActionButtonType.small: return 24.0;
+ case _FloatingActionButtonType.large: return 36.0;
+ case _FloatingActionButtonType.extended: return 24.0;
+ }
+ }
+
+ @override
+ BoxConstraints? get sizeConstraints => const BoxConstraints.tightFor(
+ width: 56.0,
+ height: 56.0,
+ );
+
+ @override
+ BoxConstraints? get smallSizeConstraints => const BoxConstraints.tightFor(
+ width: 40.0,
+ height: 40.0,
+ );
+
+ @override
+ BoxConstraints? get largeSizeConstraints => const BoxConstraints.tightFor(
+ width: 96.0,
+ height: 96.0,
+ );
+
+ @override
+ BoxConstraints? get extendedSizeConstraints => const BoxConstraints.tightFor(
+ height: 56.0,
+ );
+
+ @override double? get extendedIconLabelSpacing => 8.0;
+ @override EdgeInsetsGeometry? get extendedPadding => EdgeInsetsDirectional.only(start: hasChild && _isExtended ? 16.0 : 20.0, end: 20.0);
+ @override TextStyle? get extendedTextStyle => _textTheme.labelLarge;
+}
+
+// END GENERATED TOKEN PROPERTIES
diff --git a/packages/flutter/lib/src/material/floating_action_button_theme.dart b/packages/flutter/lib/src/material/floating_action_button_theme.dart
index 61e6338..81337cf 100644
--- a/packages/flutter/lib/src/material/floating_action_button_theme.dart
+++ b/packages/flutter/lib/src/material/floating_action_button_theme.dart
@@ -43,6 +43,7 @@
this.highlightElevation,
this.shape,
this.enableFeedback,
+ this.iconSize,
this.sizeConstraints,
this.smallSizeConstraints,
this.largeSizeConstraints,
@@ -103,6 +104,9 @@
/// ignored.
final bool? enableFeedback;
+ /// Overrides the default icon size for the [FloatingActionButton];
+ final double? iconSize;
+
/// Overrides the default size constraints for the [FloatingActionButton].
final BoxConstraints? sizeConstraints;
@@ -140,6 +144,7 @@
double? highlightElevation,
ShapeBorder? shape,
bool? enableFeedback,
+ double? iconSize,
BoxConstraints? sizeConstraints,
BoxConstraints? smallSizeConstraints,
BoxConstraints? largeSizeConstraints,
@@ -161,6 +166,7 @@
highlightElevation: highlightElevation ?? this.highlightElevation,
shape: shape ?? this.shape,
enableFeedback: enableFeedback ?? this.enableFeedback,
+ iconSize: iconSize ?? this.iconSize,
sizeConstraints: sizeConstraints ?? this.sizeConstraints,
smallSizeConstraints: smallSizeConstraints ?? this.smallSizeConstraints,
largeSizeConstraints: largeSizeConstraints ?? this.largeSizeConstraints,
@@ -193,6 +199,7 @@
highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
+ iconSize: lerpDouble(a?.iconSize, b?.iconSize, t),
sizeConstraints: BoxConstraints.lerp(a?.sizeConstraints, b?.sizeConstraints, t),
smallSizeConstraints: BoxConstraints.lerp(a?.smallSizeConstraints, b?.smallSizeConstraints, t),
largeSizeConstraints: BoxConstraints.lerp(a?.largeSizeConstraints, b?.largeSizeConstraints, t),
@@ -218,6 +225,7 @@
highlightElevation,
shape,
enableFeedback,
+ iconSize,
sizeConstraints,
smallSizeConstraints,
largeSizeConstraints,
@@ -247,6 +255,7 @@
&& other.highlightElevation == highlightElevation
&& other.shape == shape
&& other.enableFeedback == enableFeedback
+ && other.iconSize == iconSize
&& other.sizeConstraints == sizeConstraints
&& other.smallSizeConstraints == smallSizeConstraints
&& other.largeSizeConstraints == largeSizeConstraints
@@ -272,6 +281,7 @@
properties.add(DoubleProperty('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
+ properties.add(DoubleProperty('iconSize', iconSize, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('sizeConstraints', sizeConstraints, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('smallSizeConstraints', smallSizeConstraints, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('largeSizeConstraints', largeSizeConstraints, defaultValue: null));
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index dc96187..6a40f3d 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -1134,10 +1134,6 @@
/// start using new colors, typography and other features of Material 3.
/// If false, they will use the Material 2 look and feel.
///
- /// Currently no components have been migrated to support Material 3.
- /// As they are updated to include Material 3 support this documentation
- /// will be modified to indicate exactly what widgets this flag will affect.
- ///
/// During the migration to Material 3, turning this on may yield
/// inconsistent look and feel in your app. Some components will be migrated
/// before others and typography changes will be coming in stages.
@@ -1148,6 +1144,10 @@
/// all uses of it. Everything will use the Material 3 look and feel at
/// that point.
///
+ /// Components that have been migrated to Material 3 are:
+ ///
+ /// * [FloatingActionButton]
+ ///
/// See also:
///
/// * [Material Design 3](https://m3.material.io/).
diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart
index 20d895f..18322d9 100644
--- a/packages/flutter/test/material/floating_action_button_test.dart
+++ b/packages/flutter/test/material/floating_action_button_test.dart
@@ -18,6 +18,10 @@
import 'feedback_tester.dart';
void main() {
+
+ final ThemeData material3Theme = ThemeData.light().copyWith(useMaterial3: true);
+ final ThemeData material2Theme = ThemeData.light().copyWith(useMaterial3: false);
+
testWidgets('Floating Action Button control test', (WidgetTester tester) async {
bool didPressButton = false;
await tester.pumpWidget(
@@ -171,6 +175,7 @@
testWidgets('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
+ theme: material3Theme,
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () { },
@@ -183,7 +188,7 @@
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
- expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@@ -195,7 +200,7 @@
),
);
await tester.pump();
- expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
await gesture.up();
@@ -277,6 +282,7 @@
testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
+ theme: material3Theme,
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () { },
@@ -289,10 +295,11 @@
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
- expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
- const MaterialApp(
- home: Scaffold(
+ MaterialApp(
+ theme: material3Theme,
+ home: const Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: null,
),
@@ -300,11 +307,12 @@
),
);
await tester.pump();
- expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
+ theme: material3Theme,
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () { },
@@ -323,6 +331,7 @@
await tester.pumpWidget(
MaterialApp(
+ theme: material3Theme,
home: Scaffold(
body: FloatingActionButton.extended(
label: const Text('tooltip'),
@@ -359,7 +368,7 @@
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
- expect(getFABWidget(fabFinder).elevation, 12);
+ expect(getFABWidget(fabFinder).elevation, 6);
});
testWidgets('FlatActionButton mini size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
@@ -402,8 +411,9 @@
testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
await tester.pumpWidget(
- const MaterialApp(
- home: Scaffold(
+ MaterialApp(
+ theme: material3Theme,
+ home: const Scaffold(
floatingActionButton: FloatingActionButton(onPressed: null),
),
),
@@ -422,7 +432,10 @@
}
expect(getFabWidget().isExtended, false);
- expect(getRawMaterialButtonWidget().shape, const CircleBorder());
+ expect(
+ getRawMaterialButtonWidget().shape,
+ const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)))
+ );
await tester.pumpWidget(
const MaterialApp(
@@ -440,13 +453,16 @@
);
expect(getFabWidget().isExtended, true);
- expect(getRawMaterialButtonWidget().shape, const StadiumBorder());
+ expect(
+ getRawMaterialButtonWidget().shape,
+ const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)))
+ );
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
- // Verify that the widget's height is 48 and that its internal
+ // Verify that the widget's height is 56 and that its internal
/// horizontal layout is: 16 icon 8 label 20
- expect(tester.getSize(fabFinder).height, 48.0);
+ expect(tester.getSize(fabFinder).height, 56.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
@@ -479,8 +495,9 @@
}
await tester.pumpWidget(
- const MaterialApp(
- home: Scaffold(
+ MaterialApp(
+ theme: material3Theme,
+ home: const Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: SizedBox(
width: 100.0,
@@ -493,13 +510,16 @@
);
expect(getFabWidget().isExtended, true);
- expect(getRawMaterialButtonWidget().shape, const StadiumBorder());
+ expect(
+ getRawMaterialButtonWidget().shape,
+ const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)))
+ );
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsNothing);
- // Verify that the widget's height is 48 and that its internal
+ // Verify that the widget's height is 56 and that its internal
/// horizontal layout is: 20 label 20
- expect(tester.getSize(fabFinder).height, 48.0);
+ expect(tester.getSize(fabFinder).height, 56.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
@@ -770,6 +790,7 @@
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
+ theme: material3Theme,
home: Scaffold(
body: Center(
child: RepaintBoundary(
@@ -1054,6 +1075,289 @@
expect(rawMaterialButton.textStyle, style.copyWith(color: const Color(0xffffffff)));
});
+ group('Material 2', () {
+ // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
+ // is turned on by default, these tests can be removed.
+
+ testWidgets('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: () { },
+ ),
+ ),
+ ),
+ );
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ final TestGesture gesture = await tester.press(find.byType(PhysicalShape));
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: () { },
+ highlightElevation: 20.0,
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
+ await gesture.up();
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ });
+
+ testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: () { },
+ ),
+ ),
+ ),
+ );
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ await tester.press(find.byType(PhysicalShape));
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: const Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: () { },
+ ),
+ ),
+ ),
+ );
+ await tester.pump();
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ await tester.pump(const Duration(seconds: 1));
+ expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
+ });
+
+ testWidgets('Floating Action Button states elevation', (WidgetTester tester) async {
+ final FocusNode focusNode = FocusNode();
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ body: FloatingActionButton.extended(
+ label: const Text('tooltip'),
+ onPressed: () {},
+ focusNode: focusNode,
+ ),
+ ),
+ ),
+ );
+
+ final Finder fabFinder = find.byType(PhysicalShape);
+ PhysicalShape getFABWidget(Finder finder) => tester.widget<PhysicalShape>(finder);
+
+ // Default, not disabled.
+ expect(getFABWidget(fabFinder).elevation, 6);
+
+ // Focused.
+ focusNode.requestFocus();
+ await tester.pumpAndSettle();
+ expect(getFABWidget(fabFinder).elevation, 6);
+
+ // Hovered.
+ final Offset center = tester.getCenter(fabFinder);
+ final TestGesture gesture = await tester.createGesture(
+ kind: PointerDeviceKind.mouse,
+ );
+ await gesture.addPointer();
+ addTearDown(gesture.removePointer);
+ await gesture.moveTo(center);
+ await tester.pumpAndSettle();
+ expect(getFABWidget(fabFinder).elevation, 8);
+
+ // Highlighted (pressed).
+ await gesture.down(center);
+ await tester.pump(); // Start the splash and highlight animations.
+ await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
+ expect(getFABWidget(fabFinder).elevation, 12);
+ });
+
+ testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: const Scaffold(
+ floatingActionButton: FloatingActionButton(onPressed: null),
+ ),
+ ),
+ );
+
+ final Finder fabFinder = find.byType(FloatingActionButton);
+
+ FloatingActionButton getFabWidget() {
+ return tester.widget<FloatingActionButton>(fabFinder);
+ }
+
+ final Finder materialButtonFinder = find.byType(RawMaterialButton);
+
+ RawMaterialButton getRawMaterialButtonWidget() {
+ return tester.widget<RawMaterialButton>(materialButtonFinder);
+ }
+
+ expect(getFabWidget().isExtended, false);
+ expect(getRawMaterialButtonWidget().shape, const CircleBorder());
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: const Scaffold(
+ floatingActionButton: FloatingActionButton.extended(
+ label: SizedBox(
+ width: 100.0,
+ child: Text('label'),
+ ),
+ icon: Icon(Icons.android),
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(getFabWidget().isExtended, true);
+ expect(getRawMaterialButtonWidget().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);
+
+ // The overall width of the button is:
+ // 168 = 16 + 24(icon) + 8 + 100(label) + 20
+ expect(tester.getSize(find.byType(Icon)).width, 24.0);
+ expect(tester.getSize(find.text('label')).width, 100.0);
+ expect(tester.getSize(fabFinder).width, 168);
+ });
+
+ testWidgets('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async {
+ final Finder fabFinder = find.byType(FloatingActionButton);
+
+ FloatingActionButton getFabWidget() {
+ return tester.widget<FloatingActionButton>(fabFinder);
+ }
+
+ final Finder materialButtonFinder = find.byType(RawMaterialButton);
+
+ RawMaterialButton getRawMaterialButtonWidget() {
+ return tester.widget<RawMaterialButton>(materialButtonFinder);
+ }
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: const Scaffold(
+ floatingActionButton: FloatingActionButton.extended(
+ label: SizedBox(
+ width: 100.0,
+ child: Text('label'),
+ ),
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(getFabWidget().isExtended, true);
+ expect(getRawMaterialButtonWidget().shape, const StadiumBorder());
+ expect(find.text('label'), findsOneWidget);
+ expect(find.byType(Icon), findsNothing);
+
+ // Verify that the widget's height is 48 and that its internal
+ /// horizontal layout is: 20 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 labelLeft = tester.getTopLeft(find.text('label')).dx;
+ final double labelRight = tester.getTopRight(find.text('label')).dx;
+ expect(labelLeft - fabLeft, 20.0);
+ expect(fabRight - labelRight, 20.0);
+
+ // The overall width of the button is:
+ // 140 = 20 + 100(label) + 20
+ expect(tester.getSize(find.text('label')).width, 100.0);
+ expect(tester.getSize(fabFinder).width, 140);
+ });
+
+
+ // This test prevents https://github.com/flutter/flutter/issues/20483
+ testWidgets('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async {
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: material2Theme,
+ home: Scaffold(
+ body: Center(
+ child: RepaintBoundary(
+ key: key,
+ child: FloatingActionButton(
+ onPressed: () { },
+ child: const Icon(Icons.add),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await tester.press(find.byKey(key));
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 1000));
+ await expectLater(
+ find.byKey(key),
+ matchesGoldenFile('floating_action_button_test_m2.clip.png'),
+ );
+ });
+ });
+
group('feedback', () {
late FeedbackTester feedback;
diff --git a/packages/flutter/test/material/floating_action_button_theme_test.dart b/packages/flutter/test/material/floating_action_button_theme_test.dart
index 1b302a6..5cd5d27 100644
--- a/packages/flutter/test/material/floating_action_button_theme_test.dart
+++ b/packages/flutter/test/material/floating_action_button_theme_test.dart
@@ -33,6 +33,8 @@
expect(_getRawMaterialButton(tester).shape, const CircleBorder());
expect(_getRawMaterialButton(tester).splashColor, ThemeData().splashColor);
expect(_getRawMaterialButton(tester).constraints, const BoxConstraints.tightFor(width: 56.0, height: 56.0));
+ expect(_getIconSize(tester).width, 24.0);
+ expect(_getIconSize(tester).height, 24.0);
});
testWidgets('FloatingActionButtonThemeData values are used when no FloatingActionButton properties are specified', (WidgetTester tester) async {
@@ -138,6 +140,7 @@
testWidgets('FloatingActionButton.small uses custom constraints when specified in the theme', (WidgetTester tester) async {
const BoxConstraints constraints = BoxConstraints.tightFor(width: 100.0, height: 100.0);
+ const double iconSize = 24.0;
await tester.pumpWidget(MaterialApp(
theme: ThemeData().copyWith(
@@ -154,10 +157,13 @@
));
expect(_getRawMaterialButton(tester).constraints, constraints);
+ expect(_getIconSize(tester).width, iconSize);
+ expect(_getIconSize(tester).height, iconSize);
});
testWidgets('FloatingActionButton.large uses custom constraints when specified in the theme', (WidgetTester tester) async {
const BoxConstraints constraints = BoxConstraints.tightFor(width: 100.0, height: 100.0);
+ const double iconSize = 36.0;
await tester.pumpWidget(MaterialApp(
theme: ThemeData().copyWith(
@@ -174,6 +180,8 @@
));
expect(_getRawMaterialButton(tester).constraints, constraints);
+ expect(_getIconSize(tester).width, iconSize);
+ expect(_getIconSize(tester).height, iconSize);
});
testWidgets('FloatingActionButton.extended uses custom properties when specified in the theme', (WidgetTester tester) async {
@@ -271,6 +279,7 @@
highlightElevation: 43,
shape: BeveledRectangleBorder(),
enableFeedback: true,
+ iconSize: 42,
sizeConstraints: BoxConstraints.tightFor(width: 100.0, height: 100.0),
smallSizeConstraints: BoxConstraints.tightFor(width: 101.0, height: 101.0),
largeSizeConstraints: BoxConstraints.tightFor(width: 102.0, height: 102.0),
@@ -298,6 +307,7 @@
'highlightElevation: 43.0',
'shape: BeveledRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)',
'enableFeedback: true',
+ 'iconSize: 42.0',
'sizeConstraints: BoxConstraints(w=100.0, h=100.0)',
'smallSizeConstraints: BoxConstraints(w=101.0, h=101.0)',
'largeSizeConstraints: BoxConstraints(w=102.0, h=102.0)',
@@ -326,3 +336,15 @@
),
);
}
+
+SizedBox _getIconSize(WidgetTester tester) {
+ return tester.widget<SizedBox>(
+ find.descendant(
+ of: find.descendant(
+ of: find.byType(FloatingActionButton),
+ matching: find.byType(Icon),
+ ),
+ matching: find.byType(SizedBox),
+ ),
+ );
+}