From 2ab2ca2bc620e3e99711b3d2a11d08f3c94b73ba Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Wed, 12 Jan 2022 19:15:15 -0800 Subject: [PATCH] Migrate FloatingActionButton to Material 3 (#94486) --- .../gen_defaults/data/material-tokens.json | 4 +- .../src/material/floating_action_button.dart | 226 +++++++++--- .../floating_action_button_theme.dart | 10 + .../flutter/lib/src/material/theme_data.dart | 8 +- .../material/floating_action_button_test.dart | 340 +++++++++++++++++- .../floating_action_button_theme_test.dart | 22 ++ 6 files changed, 536 insertions(+), 74 deletions(-) diff --git a/dev/tools/gen_defaults/data/material-tokens.json b/dev/tools/gen_defaults/data/material-tokens.json index 341259797fa..2cd2616ffd4 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 0b7fefddf11..fcb6c32e35f 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/rendering.dart'; 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 @@ class FloatingActionButton extends StatelessWidget { 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 @@ class _RenderChildOverflowBox extends RenderAligningShiftedBox { } } } + +// 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 61e63382e06..81337cf0b9e 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 @@ class FloatingActionButtonThemeData with Diagnosticable { this.highlightElevation, this.shape, this.enableFeedback, + this.iconSize, this.sizeConstraints, this.smallSizeConstraints, this.largeSizeConstraints, @@ -103,6 +104,9 @@ class FloatingActionButtonThemeData with Diagnosticable { /// 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 @@ class FloatingActionButtonThemeData with Diagnosticable { double? highlightElevation, ShapeBorder? shape, bool? enableFeedback, + double? iconSize, BoxConstraints? sizeConstraints, BoxConstraints? smallSizeConstraints, BoxConstraints? largeSizeConstraints, @@ -161,6 +166,7 @@ class FloatingActionButtonThemeData with Diagnosticable { 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 @@ class FloatingActionButtonThemeData with Diagnosticable { 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 @@ class FloatingActionButtonThemeData with Diagnosticable { highlightElevation, shape, enableFeedback, + iconSize, sizeConstraints, smallSizeConstraints, largeSizeConstraints, @@ -247,6 +255,7 @@ class FloatingActionButtonThemeData with Diagnosticable { && 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 @@ class FloatingActionButtonThemeData with Diagnosticable { properties.add(DoubleProperty('highlightElevation', highlightElevation, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); + properties.add(DoubleProperty('iconSize', iconSize, defaultValue: null)); properties.add(DiagnosticsProperty('sizeConstraints', sizeConstraints, defaultValue: null)); properties.add(DiagnosticsProperty('smallSizeConstraints', smallSizeConstraints, defaultValue: null)); properties.add(DiagnosticsProperty('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 dc96187718b..6a40f3d93d4 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1134,10 +1134,6 @@ class ThemeData with Diagnosticable { /// 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 @@ class ThemeData with Diagnosticable { /// 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 20d895f3a21..18322d97c73 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 '../widgets/semantics_tester.dart'; 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 @@ void main() { 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 @@ void main() { await tester.pump(); expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pump(const Duration(seconds: 1)); - expect(tester.widget(find.byType(PhysicalShape)).elevation, 12.0); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -195,7 +200,7 @@ void main() { ), ); await tester.pump(); - expect(tester.widget(find.byType(PhysicalShape)).elevation, 12.0); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pump(const Duration(seconds: 1)); expect(tester.widget(find.byType(PhysicalShape)).elevation, 20.0); await gesture.up(); @@ -277,6 +282,7 @@ void main() { 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 @@ void main() { await tester.pump(); expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pump(const Duration(seconds: 1)); - expect(tester.widget(find.byType(PhysicalShape)).elevation, 12.0); + expect(tester.widget(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 @@ void main() { ), ); await tester.pump(); - expect(tester.widget(find.byType(PhysicalShape)).elevation, 12.0); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pump(const Duration(seconds: 1)); expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); await tester.pumpWidget( MaterialApp( + theme: material3Theme, home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { }, @@ -323,6 +331,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: material3Theme, home: Scaffold( body: FloatingActionButton.extended( label: const Text('tooltip'), @@ -359,7 +368,7 @@ void main() { 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 @@ void main() { 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 @@ void main() { } 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 @@ void main() { ); 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 @@ void main() { } 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 @@ void main() { ); 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 @@ void main() { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( + theme: material3Theme, home: Scaffold( body: Center( child: RepaintBoundary( @@ -1054,6 +1075,289 @@ void main() { 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(find.byType(PhysicalShape)).elevation, 6.0); + final TestGesture gesture = await tester.press(find.byType(PhysicalShape)); + await tester.pump(); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(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(find.byType(PhysicalShape)).elevation, 12.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 20.0); + await gesture.up(); + await tester.pump(); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 20.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(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(find.byType(PhysicalShape)).elevation, 6.0); + await tester.press(find.byType(PhysicalShape)); + await tester.pump(); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(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(find.byType(PhysicalShape)).elevation, 12.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); + await tester.pumpWidget( + MaterialApp( + theme: material2Theme, + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + ), + ), + ), + ); + await tester.pump(); + expect(tester.widget(find.byType(PhysicalShape)).elevation, 6.0); + await tester.pump(const Duration(seconds: 1)); + expect(tester.widget(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(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(fabFinder); + } + + final Finder materialButtonFinder = find.byType(RawMaterialButton); + + RawMaterialButton getRawMaterialButtonWidget() { + return tester.widget(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(fabFinder); + } + + final Finder materialButtonFinder = find.byType(RawMaterialButton); + + RawMaterialButton getRawMaterialButtonWidget() { + return tester.widget(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 1b302a60fa8..5cd5d27c375 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { )); 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 @@ void main() { )); 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 @@ void main() { 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 @@ void main() { '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 @@ RichText _getRichText(WidgetTester tester) { ), ); } + +SizedBox _getIconSize(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.descendant( + of: find.byType(FloatingActionButton), + matching: find.byType(Icon), + ), + matching: find.byType(SizedBox), + ), + ); +}