Unverified Commit 5454bab6 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Added support for M3 filled and filled tonal buttons. (#107382)

parent e6ed4c46
...@@ -103,6 +103,8 @@ Future<void> main(List<String> args) async { ...@@ -103,6 +103,8 @@ Future<void> main(List<String> args) async {
AppBarTemplate('AppBar', '$materialLib/app_bar.dart', tokens).updateFile(); AppBarTemplate('AppBar', '$materialLib/app_bar.dart', tokens).updateFile();
ButtonTemplate('md.comp.elevated-button', 'ElevatedButton', '$materialLib/elevated_button.dart', tokens).updateFile(); ButtonTemplate('md.comp.elevated-button', 'ElevatedButton', '$materialLib/elevated_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.filled-button', 'FilledButton', '$materialLib/filled_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.filled-tonal-button', 'FilledTonalButton', '$materialLib/filled_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.outlined-button', 'OutlinedButton', '$materialLib/outlined_button.dart', tokens).updateFile(); ButtonTemplate('md.comp.outlined-button', 'OutlinedButton', '$materialLib/outlined_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.text-button', 'TextButton', '$materialLib/text_button.dart', tokens).updateFile(); ButtonTemplate('md.comp.text-button', 'TextButton', '$materialLib/text_button.dart', tokens).updateFile();
CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile(); CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile();
......
...@@ -58,31 +58,9 @@ class ButtonTypesGroup extends StatelessWidget { ...@@ -58,31 +58,9 @@ class ButtonTypesGroup extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[ children: <Widget>[
ElevatedButton(onPressed: onPressed, child: const Text('Elevated')), ElevatedButton(onPressed: onPressed, child: const Text('Elevated')),
FilledButton(onPressed: onPressed, child: const Text('Filled')),
// Use an ElevatedButton with specific style to implement the FilledButton.tonal(onPressed: onPressed, child: const Text('Filled Tonal')),
// 'Filled' type.
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: onPressed,
child: const Text('Filled'),
),
// Use an ElevatedButton with specific style to implement the
// 'Filled Tonal' type.
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: onPressed,
child: const Text('Filled Tonal'),
),
OutlinedButton(onPressed: onPressed, child: const Text('Outlined')), OutlinedButton(onPressed: onPressed, child: const Text('Outlined')),
TextButton(onPressed: onPressed, child: const Text('Text')), TextButton(onPressed: onPressed, child: const Text('Text')),
], ],
), ),
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for FilledButton
import 'package:flutter/material.dart';
void main() {
runApp(const FilledButtonApp());
}
class FilledButtonApp extends StatelessWidget {
const FilledButtonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('FilledButton Sample')),
body: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(children: <Widget>[
const SizedBox(height: 30),
const Text('Filled'),
const SizedBox(height: 15),
FilledButton(
onPressed: () {},
child: const Text('Enabled'),
),
const SizedBox(height: 30),
const FilledButton(
onPressed: null,
child: Text('Disabled'),
),
]),
const SizedBox(width: 30),
Column(children: <Widget>[
const SizedBox(height: 30),
const Text('Filled tonal'),
const SizedBox(height: 15),
FilledButton.tonal(
onPressed: () {},
child: const Text('Enabled'),
),
const SizedBox(height: 30),
const FilledButton.tonal(
onPressed: null,
child: Text('Disabled'),
),
])
],
),
),
),
);
}
}
...@@ -80,6 +80,8 @@ export 'src/material/expansion_panel.dart'; ...@@ -80,6 +80,8 @@ export 'src/material/expansion_panel.dart';
export 'src/material/expansion_tile.dart'; export 'src/material/expansion_tile.dart';
export 'src/material/expansion_tile_theme.dart'; export 'src/material/expansion_tile_theme.dart';
export 'src/material/feedback.dart'; export 'src/material/feedback.dart';
export 'src/material/filled_button.dart';
export 'src/material/filled_button_theme.dart';
export 'src/material/filter_chip.dart'; export 'src/material/filter_chip.dart';
export 'src/material/flexible_space_bar.dart'; export 'src/material/flexible_space_bar.dart';
export 'src/material/floating_action_button.dart'; export 'src/material/floating_action_button.dart';
......
...@@ -26,14 +26,16 @@ import 'theme_data.dart'; ...@@ -26,14 +26,16 @@ import 'theme_data.dart';
/// from the themes or from app-specific sources. /// from the themes or from app-specific sources.
/// ///
/// This class is planned to be deprecated in a future release, see /// This class is planned to be deprecated in a future release, see
/// [ButtonStyleButton], the base class of [TextButton], [ElevatedButton], and /// [ButtonStyleButton], the base class of [ElevatedButton], [FilledButton],
/// [OutlinedButton]. /// [OutlinedButton] and [TextButton].
/// ///
/// See also: /// See also:
/// ///
/// * [TextButton], a simple flat button without a shadow.
/// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [OutlinedButton], a [TextButton] with a border outline. /// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
@Category(<String>['Material', 'Button']) @Category(<String>['Material', 'Button'])
class RawMaterialButton extends StatefulWidget { class RawMaterialButton extends StatefulWidget {
/// Create a button based on [Semantics], [Material], and [InkWell] widgets. /// Create a button based on [Semantics], [Material], and [InkWell] widgets.
......
...@@ -78,8 +78,8 @@ import 'theme_data.dart'; ...@@ -78,8 +78,8 @@ import 'theme_data.dart';
/// useful to make relatively sweeping changes based on a few initial /// useful to make relatively sweeping changes based on a few initial
/// parameters with simple values. The button styleFrom() methods /// parameters with simple values. The button styleFrom() methods
/// enable such sweeping changes. See for example: /// enable such sweeping changes. See for example:
/// [TextButton.styleFrom], [ElevatedButton.styleFrom], /// [ElevatedButton.styleFrom], [FilledButton.styleFrom],
/// [OutlinedButton.styleFrom]. /// [OutlinedButton.styleFrom], [TextButton.styleFrom].
/// ///
/// For example, to override the default text and icon colors for a /// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the /// [TextButton], as well as its overlay color, with all of the
...@@ -119,8 +119,8 @@ import 'theme_data.dart'; ...@@ -119,8 +119,8 @@ import 'theme_data.dart';
/// | Type | Flutter implementation | /// | Type | Flutter implementation |
/// | :----------- | :---------------------- | /// | :----------- | :---------------------- |
/// | Elevated | [ElevatedButton] | /// | Elevated | [ElevatedButton] |
/// | Filled | Styled [ElevatedButton] | /// | Filled | [FilledButton] |
/// | Filled Tonal | Styled [ElevatedButton] | /// | Filled Tonal | [FilledButton.tonal] |
/// | Outlined | [OutlinedButton] | /// | Outlined | [OutlinedButton] |
/// | Text | [TextButton] | /// | Text | [TextButton] |
/// ///
...@@ -132,9 +132,10 @@ import 'theme_data.dart'; ...@@ -132,9 +132,10 @@ import 'theme_data.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [TextButtonTheme], the theme for [TextButton]s.
/// * [ElevatedButtonTheme], the theme for [ElevatedButton]s. /// * [ElevatedButtonTheme], the theme for [ElevatedButton]s.
/// * [FilledButtonTheme], the theme for [FilledButton]s.
/// * [OutlinedButtonTheme], the theme for [OutlinedButton]s. /// * [OutlinedButtonTheme], the theme for [OutlinedButton]s.
/// * [TextButtonTheme], the theme for [TextButton]s.
@immutable @immutable
class ButtonStyle with Diagnosticable { class ButtonStyle with Diagnosticable {
/// Create a [ButtonStyle]. /// Create a [ButtonStyle].
......
...@@ -21,10 +21,13 @@ import 'theme_data.dart'; ...@@ -21,10 +21,13 @@ import 'theme_data.dart';
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf]. /// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
/// ///
/// See also: /// See also:
/// /// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [TextButton], a simple ButtonStyleButton without a shadow. /// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [ElevatedButton], a filled ButtonStyleButton whose material elevates when pressed. /// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], similar to [TextButton], but with an outline. /// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
/// the Material Design button types and how they should be used in designs.
abstract class ButtonStyleButton extends StatefulWidget { abstract class ButtonStyleButton extends StatefulWidget {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
...@@ -191,9 +194,10 @@ abstract class ButtonStyleButton extends StatefulWidget { ...@@ -191,9 +194,10 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// See also: /// See also:
/// ///
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State]. /// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
/// * [TextButton], a simple button without a shadow.
/// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline. /// * [OutlinedButton], similar to [TextButton], but with an outline.
/// * [TextButton], a simple button without a shadow.
class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStateMixin { class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStateMixin {
AnimationController? controller; AnimationController? controller;
double? elevation; double? elevation;
......
...@@ -48,9 +48,10 @@ enum ButtonBarLayoutBehavior { ...@@ -48,9 +48,10 @@ enum ButtonBarLayoutBehavior {
/// This class is planned to be deprecated in a future release. /// This class is planned to be deprecated in a future release.
/// Please use one or more of these buttons and associated themes instead: /// Please use one or more of these buttons and associated themes instead:
/// ///
/// * [TextButton], [TextButtonTheme], [TextButtonThemeData],
/// * [ElevatedButton], [ElevatedButtonTheme], [ElevatedButtonThemeData], /// * [ElevatedButton], [ElevatedButtonTheme], [ElevatedButtonThemeData],
/// * [FilledButton], [FilledButtonTheme], [FilledButtonThemeData],
/// * [OutlinedButton], [OutlinedButtonTheme], [OutlinedButtonThemeData] /// * [OutlinedButton], [OutlinedButtonTheme], [OutlinedButtonThemeData]
/// * [TextButton], [TextButtonTheme], [TextButtonThemeData],
/// ///
/// A button theme can be specified as part of the overall Material theme /// A button theme can be specified as part of the overall Material theme
/// using [ThemeData.buttonTheme]. The Material theme's button theme data /// using [ThemeData.buttonTheme]. The Material theme's button theme data
......
...@@ -54,8 +54,10 @@ import 'theme_data.dart'; ...@@ -54,8 +54,10 @@ import 'theme_data.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [TextButton], a simple flat button without a shadow. /// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [OutlinedButton], a [TextButton] with a border outline. /// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://material.io/design/components/buttons.html> /// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons> /// * <https://m3.material.io/components/buttons>
class ElevatedButton extends ButtonStyleButton { class ElevatedButton extends ButtonStyleButton {
......
// Copyright 2014 The Flutter 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:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'constants.dart';
import 'filled_button_theme.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
enum _FilledButtonVariant { filled, tonal }
/// A Material Design filled button.
///
/// Filled buttons have the most visual impact after the [FloatingActionButton],
/// and should be used for important, final actions that complete a flow,
/// like **Save**, **Join now**, or **Confirm**.
///
/// A filled button is a label [child] displayed on a [Material]
/// widget. The label's [Text] and [Icon] widgets are displayed in
/// [style]'s [ButtonStyle.foregroundColor] and the button's filled
/// background is the [ButtonStyle.backgroundColor].
///
/// The filled button's default style is defined by
/// [defaultStyleOf]. The style of this filled button can be
/// overridden with its [style] parameter. The style of all filled
/// buttons in a subtree can be overridden with the
/// [FilledButtonTheme], and the style of all of the filled
/// buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.filledButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// filled button [ButtonStyle] from simple values.
///
/// If [onPressed] and [onLongPress] callbacks are null, then the
/// button will be disabled.
///
/// To create a 'filled tonal' button, use [FilledButton.tonal].
///
/// {@tool dartpad}
/// This sample produces enabled and disabled filled and filled tonal
/// buttons.
///
/// ** See code in examples/api/lib/material/filled_button/filled_button.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons>
class FilledButton extends ButtonStyleButton {
/// Create a FilledButton.
///
/// The [autofocus] and [clipBehavior] arguments must not be null.
const FilledButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
}) : _variant = _FilledButtonVariant.filled;
/// Create a filled button from [icon] and [label].
///
/// The icon and label are arranged in a row with padding at the start and end
/// and a gap between them.
///
/// The [icon] and [label] arguments must not be null.
factory FilledButton.icon({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
Clip? clipBehavior,
required Widget icon,
required Widget label,
}) = _FilledButtonWithIcon;
/// Create a tonal variant of FilledButton.
///
/// A filled tonal button is an alternative middle ground between
/// [FilledButton] and [OutlinedButton]. They’re useful in contexts where
/// a lower-priority button requires slightly more emphasis than an
/// outline would give, such as "Next" in an onboarding flow.
///
/// The [autofocus] and [clipBehavior] arguments must not be null.
const FilledButton.tonal({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
}) : _variant = _FilledButtonVariant.tonal;
/// Create a filled tonal button from [icon] and [label].
///
/// The icon and label are arranged in a row with padding at the start and end
/// and a gap between them.
///
/// The [icon] and [label] arguments must not be null.
factory FilledButton.tonalIcon({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
Clip? clipBehavior,
required Widget icon,
required Widget label,
}) {
return _FilledButtonWithIcon.tonal(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus,
clipBehavior: clipBehavior,
icon: icon,
label: label,
);
}
/// A static convenience method that constructs a filled button
/// [ButtonStyle] given simple values.
///
/// The [foregroundColor], and [disabledForegroundColor] colors are used to create a
/// [MaterialStateProperty] [ButtonStyle.foregroundColor] value. The
/// [backgroundColor] and [disabledBackgroundColor] are used to create a
/// [MaterialStateProperty] [ButtonStyle.backgroundColor] value.
///
/// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
///
/// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all
/// states.
///
/// All parameters default to null, by default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default text and icon colors for a
/// [FilledButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// FilledButton(
/// style: FilledButton.styleFrom(foregroundColor: Colors.green),
/// onPressed: () {},
/// child: const Text('Filled button'),
/// );
/// ```
///
/// or for a Filled tonal variant:
/// ```dart
/// FilledButton.tonal(
/// style: FilledButton.styleFrom(foregroundColor: Colors.green),
/// onPressed: () {},
/// child: const Text('Filled tonal button'),
/// );
/// ```
static ButtonStyle styleFrom({
Color? foregroundColor,
Color? backgroundColor,
Color? disabledForegroundColor,
Color? disabledBackgroundColor,
Color? shadowColor,
Color? surfaceTintColor,
double? elevation,
TextStyle? textStyle,
EdgeInsetsGeometry? padding,
Size? minimumSize,
Size? fixedSize,
Size? maximumSize,
BorderSide? side,
OutlinedBorder? shape,
MouseCursor? enabledMouseCursor,
MouseCursor? disabledMouseCursor,
VisualDensity? visualDensity,
MaterialTapTargetSize? tapTargetSize,
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? backgroundColorProp =
(backgroundColor == null && disabledBackgroundColor == null)
? null
: _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor);
final Color? foreground = foregroundColor;
final Color? disabledForeground = disabledForegroundColor;
final MaterialStateProperty<Color?>? foregroundColorProp =
(foreground == null && disabledForeground == null)
? null
: _FilledButtonDefaultColor(foreground, disabledForeground);
final MaterialStateProperty<Color?>? overlayColor = (foreground == null)
? null
: _FilledButtonDefaultOverlay(foreground);
final MaterialStateProperty<MouseCursor?>? mouseCursor =
(enabledMouseCursor == null && disabledMouseCursor == null)
? null
: _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp,
overlayColor: overlayColor,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
elevation: ButtonStyleButton.allOrNull(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
);
}
final _FilledButtonVariant _variant;
/// Defines the button's default appearance.
///
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color.
///
/// All of the ButtonStyle's defaults appear below. In this list
/// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
/// scheme values like "onSurface(0.38)" are shorthand for
/// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
/// properties that are not followed by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state, and "others" means all other states.
///
/// The `textScaleFactor` is the value of
/// `MediaQuery.of(context).textScaleFactor` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
/// The color of the [ButtonStyle.textStyle] is not used, the
/// [ButtonStyle.foregroundColor] color is used instead.
///
/// * `textStyle` - Theme.textTheme.labelLarge
/// * `backgroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.12)
/// * others - Theme.colorScheme.secondaryContainer
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.onSecondaryContainer
/// * `overlayColor`
/// * hovered - Theme.colorScheme.onSecondaryContainer(0.08)
/// * focused or pressed - Theme.colorScheme.onSecondaryContainer(0.12)
/// * `shadowColor` - Theme.colorScheme.shadow
/// * `surfaceTintColor` - null
/// * `elevation`
/// * disabled - 0
/// * default - 0
/// * hovered - 1
/// * focused or pressed - 0
/// * `padding`
/// * `textScaleFactor <= 1` - horizontal(16)
/// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
/// * `minimumSize` - Size(64, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - StadiumBorder()
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - Theme.visualDensity
/// * `tapTargetSize` - Theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
///
/// The default padding values for the [FilledButton.icon] factory are slightly different:
///
/// * `padding`
/// * `textScaleFactor <= 1` - start(12) end(16)
/// * `1 < textScaleFactor <= 2` - lerp(start(12) end(16), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
///
/// The default value for `side`, which defines the appearance of the button's
/// outline, is null. That means that the outline is defined by the button
/// shape's [OutlinedBorder.side]. Typically the default value of an
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
///
@override
ButtonStyle defaultStyleOf(BuildContext context) {
switch (_variant) {
case _FilledButtonVariant.filled:
return _FilledButtonDefaultsM3(context);
case _FilledButtonVariant.tonal:
return _FilledTonalButtonDefaultsM3(context);
}
}
/// Returns the [FilledButtonThemeData.style] of the closest
/// [FilledButtonTheme] ancestor.
@override
ButtonStyle? themeStyleOf(BuildContext context) {
return FilledButtonTheme.of(context).style;
}
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
return ButtonStyleButton.scaledPadding(
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
);
}
@immutable
class _FilledButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_FilledButtonDefaultColor(this.color, this.disabled);
final Color? color;
final Color? disabled;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
return color;
}
}
@immutable
class _FilledButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
_FilledButtonDefaultOverlay(this.overlay);
final Color overlay;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return overlay.withOpacity(0.08);
}
if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) {
return overlay.withOpacity(0.12);
}
return null;
}
}
@immutable
class _FilledButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_FilledButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor? enabledCursor;
final MouseCursor? disabledCursor;
@override
MouseCursor? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
class _FilledButtonWithIcon extends FilledButton {
_FilledButtonWithIcon({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
bool? autofocus,
Clip? clipBehavior,
required Widget icon,
required Widget label,
}) : assert(icon != null),
assert(label != null),
super(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _FilledButtonWithIconChild(icon: icon, label: label)
);
_FilledButtonWithIcon.tonal({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
bool? autofocus,
Clip? clipBehavior,
required Widget icon,
required Widget label,
}) : assert(icon != null),
assert(label != null),
super.tonal(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _FilledButtonWithIconChild(icon: icon, label: label)
);
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
);
}
}
class _FilledButtonWithIconChild extends StatelessWidget {
const _FilledButtonWithIconChild({ required this.label, required this.icon });
final Widget label;
final Widget icon;
@override
Widget build(BuildContext context) {
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
// Adjust the gap based on the text scale factor. Start at 8, and lerp
// to 4 based on how large the text is.
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
);
}
}
// BEGIN GENERATED TOKEN PROPERTIES - FilledButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_101
class _FilledButtonDefaultsM3 extends ButtonStyle {
_FilledButtonDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
MaterialStateProperty<TextStyle?> get textStyle =>
MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
@override
MaterialStateProperty<Color?>? get backgroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.12);
}
return _colors.primary;
});
@override
MaterialStateProperty<Color?>? get foregroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.onPrimary;
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return _colors.onPrimary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onPrimary.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onPrimary.withOpacity(0.12);
}
return null;
});
@override
MaterialStateProperty<Color>? get shadowColor =>
ButtonStyleButton.allOrNull<Color>(_colors.shadow);
// No default surface tint color
@override
MaterialStateProperty<double>? get elevation =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return 0.0;
}
if (states.contains(MaterialState.hovered)) {
return 1.0;
}
if (states.contains(MaterialState.focused)) {
return 0.0;
}
if (states.contains(MaterialState.pressed)) {
return 0.0;
}
return 0.0;
});
@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(_scaledPadding(context));
@override
MaterialStateProperty<Size>? get minimumSize =>
ButtonStyleButton.allOrNull<Size>(const Size(64.0, 40.0));
// No default fixedSize
@override
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
// No default side
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
ButtonStyleButton.allOrNull<OutlinedBorder>(const StadiumBorder());
@override
MaterialStateProperty<MouseCursor?>? get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// END GENERATED TOKEN PROPERTIES - FilledButton
// BEGIN GENERATED TOKEN PROPERTIES - FilledTonalButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_101
class _FilledTonalButtonDefaultsM3 extends ButtonStyle {
_FilledTonalButtonDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
MaterialStateProperty<TextStyle?> get textStyle =>
MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
@override
MaterialStateProperty<Color?>? get backgroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.12);
}
return _colors.secondaryContainer;
});
@override
MaterialStateProperty<Color?>? get foregroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.onSecondaryContainer;
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return _colors.onSecondaryContainer.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSecondaryContainer.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSecondaryContainer.withOpacity(0.12);
}
return null;
});
@override
MaterialStateProperty<Color>? get shadowColor =>
ButtonStyleButton.allOrNull<Color>(_colors.shadow);
// No default surface tint color
@override
MaterialStateProperty<double>? get elevation =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return 0.0;
}
if (states.contains(MaterialState.hovered)) {
return 1.0;
}
if (states.contains(MaterialState.focused)) {
return 0.0;
}
if (states.contains(MaterialState.pressed)) {
return 0.0;
}
return 0.0;
});
@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(_scaledPadding(context));
@override
MaterialStateProperty<Size>? get minimumSize =>
ButtonStyleButton.allOrNull<Size>(const Size(64.0, 40.0));
// No default fixedSize
@override
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
// No default side
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
ButtonStyleButton.allOrNull<OutlinedBorder>(const StadiumBorder());
@override
MaterialStateProperty<MouseCursor?>? get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// END GENERATED TOKEN PROPERTIES - FilledTonalButton
// Copyright 2014 The Flutter 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// A [ButtonStyle] that overrides the default appearance of
/// [FilledButton]s when it's used with [FilledButtonTheme] or with the
/// overall [Theme]'s [ThemeData.filledButtonTheme].
///
/// The [style]'s properties override [FilledButton]'s default style,
/// i.e. the [ButtonStyle] returned by [FilledButton.defaultStyleOf]. Only
/// the style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [FilledButtonTheme], the theme which is configured with this class.
/// * [FilledButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for text buttons.
/// * [FilledButton.styleFrom], which converts simple values into a
/// [ButtonStyle] that's consistent with [FilledButton]'s defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property
/// to a simple value based on a set of [MaterialState]s.
/// * [ThemeData.filledButtonTheme], which can be used to override the default
/// [ButtonStyle] for [FilledButton]s below the overall [Theme].
@immutable
class FilledButtonThemeData with Diagnosticable {
/// Creates an [FilledButtonThemeData].
///
/// The [style] may be null.
const FilledButtonThemeData({ this.style });
/// Overrides for [FilledButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the [ButtonStyle] returned by
/// [FilledButton.defaultStyleOf].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle? style;
/// Linearly interpolate between two filled button themes.
static FilledButtonThemeData? lerp(FilledButtonThemeData? a, FilledButtonThemeData? b, double t) {
assert (t != null);
if (a == null && b == null) {
return null;
}
return FilledButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
);
}
@override
int get hashCode => style.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is FilledButtonThemeData && other.style == style;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
}
}
/// Overrides the default [ButtonStyle] of its [FilledButton] descendants.
///
/// See also:
///
/// * [FilledButtonThemeData], which is used to configure this theme.
/// * [FilledButton.defaultStyleOf], which returns the default [ButtonStyle]
/// for filled buttons.
/// * [FilledButton.styleFrom], which converts simple values into a
/// [ButtonStyle] that's consistent with [FilledButton]'s defaults.
/// * [ThemeData.filledButtonTheme], which can be used to override the default
/// [ButtonStyle] for [FilledButton]s below the overall [Theme].
class FilledButtonTheme extends InheritedTheme {
/// Create a [FilledButtonTheme].
///
/// The [data] parameter must not be null.
const FilledButtonTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// The configuration of this theme.
final FilledButtonThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [FilledButtonTheme] widget, then
/// [ThemeData.filledButtonTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// FilledButtonThemeData theme = FilledButtonTheme.of(context);
/// ```
static FilledButtonThemeData of(BuildContext context) {
final FilledButtonTheme? buttonTheme = context.dependOnInheritedWidgetOfExactType<FilledButtonTheme>();
return buttonTheme?.data ?? Theme.of(context).filledButtonTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return FilledButtonTheme(data: data, child: child);
}
@override
bool updateShouldNotify(FilledButtonTheme oldWidget) => data != oldWidget.data;
}
...@@ -59,8 +59,10 @@ import 'theme_data.dart'; ...@@ -59,8 +59,10 @@ import 'theme_data.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [ElevatedButton], a filled Material Design button with a shadow. /// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [TextButton], a Material Design button without a shadow. /// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://material.io/design/components/buttons.html> /// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons> /// * <https://m3.material.io/components/buttons>
class OutlinedButton extends ButtonStyleButton { class OutlinedButton extends ButtonStyleButton {
......
...@@ -66,8 +66,10 @@ import 'theme_data.dart'; ...@@ -66,8 +66,10 @@ import 'theme_data.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [OutlinedButton], a [TextButton] with a border outline.
/// * [ElevatedButton], a filled button whose material elevates when pressed. /// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * <https://material.io/design/components/buttons.html> /// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons> /// * <https://m3.material.io/components/buttons>
class TextButton extends ButtonStyleButton { class TextButton extends ButtonStyleButton {
......
...@@ -26,6 +26,7 @@ import 'divider_theme.dart'; ...@@ -26,6 +26,7 @@ import 'divider_theme.dart';
import 'drawer_theme.dart'; import 'drawer_theme.dart';
import 'elevated_button_theme.dart'; import 'elevated_button_theme.dart';
import 'expansion_tile_theme.dart'; import 'expansion_tile_theme.dart';
import 'filled_button_theme.dart';
import 'floating_action_button_theme.dart'; import 'floating_action_button_theme.dart';
import 'icon_button_theme.dart'; import 'icon_button_theme.dart';
import 'ink_ripple.dart'; import 'ink_ripple.dart';
...@@ -343,6 +344,7 @@ class ThemeData with Diagnosticable { ...@@ -343,6 +344,7 @@ class ThemeData with Diagnosticable {
DrawerThemeData? drawerTheme, DrawerThemeData? drawerTheme,
ElevatedButtonThemeData? elevatedButtonTheme, ElevatedButtonThemeData? elevatedButtonTheme,
ExpansionTileThemeData? expansionTileTheme, ExpansionTileThemeData? expansionTileTheme,
FilledButtonThemeData? filledButtonTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme, IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme, ListTileThemeData? listTileTheme,
...@@ -564,6 +566,7 @@ class ThemeData with Diagnosticable { ...@@ -564,6 +566,7 @@ class ThemeData with Diagnosticable {
dividerTheme ??= const DividerThemeData(); dividerTheme ??= const DividerThemeData();
drawerTheme ??= const DrawerThemeData(); drawerTheme ??= const DrawerThemeData();
elevatedButtonTheme ??= const ElevatedButtonThemeData(); elevatedButtonTheme ??= const ElevatedButtonThemeData();
filledButtonTheme ??= const FilledButtonThemeData();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData(); floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
iconButtonTheme ??= const IconButtonThemeData(); iconButtonTheme ??= const IconButtonThemeData();
listTileTheme ??= const ListTileThemeData(); listTileTheme ??= const ListTileThemeData();
...@@ -655,6 +658,7 @@ class ThemeData with Diagnosticable { ...@@ -655,6 +658,7 @@ class ThemeData with Diagnosticable {
drawerTheme: drawerTheme, drawerTheme: drawerTheme,
elevatedButtonTheme: elevatedButtonTheme, elevatedButtonTheme: elevatedButtonTheme,
expansionTileTheme: expansionTileTheme, expansionTileTheme: expansionTileTheme,
filledButtonTheme: filledButtonTheme,
floatingActionButtonTheme: floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme, iconButtonTheme: iconButtonTheme,
listTileTheme: listTileTheme, listTileTheme: listTileTheme,
...@@ -761,6 +765,7 @@ class ThemeData with Diagnosticable { ...@@ -761,6 +765,7 @@ class ThemeData with Diagnosticable {
required this.drawerTheme, required this.drawerTheme,
required this.elevatedButtonTheme, required this.elevatedButtonTheme,
required this.expansionTileTheme, required this.expansionTileTheme,
required this.filledButtonTheme,
required this.floatingActionButtonTheme, required this.floatingActionButtonTheme,
required this.iconButtonTheme, required this.iconButtonTheme,
required this.listTileTheme, required this.listTileTheme,
...@@ -909,6 +914,7 @@ class ThemeData with Diagnosticable { ...@@ -909,6 +914,7 @@ class ThemeData with Diagnosticable {
assert(drawerTheme != null), assert(drawerTheme != null),
assert(elevatedButtonTheme != null), assert(elevatedButtonTheme != null),
assert(expansionTileTheme != null), assert(expansionTileTheme != null),
assert(filledButtonTheme != null),
assert(floatingActionButtonTheme != null), assert(floatingActionButtonTheme != null),
assert(iconButtonTheme != null), assert(iconButtonTheme != null),
assert(listTileTheme != null), assert(listTileTheme != null),
...@@ -1211,7 +1217,7 @@ class ThemeData with Diagnosticable { ...@@ -1211,7 +1217,7 @@ class ThemeData with Diagnosticable {
/// * Typography: `typography` (see table above) /// * Typography: `typography` (see table above)
/// ///
/// ### Components /// ### Components
/// * Common buttons: [TextButton], [OutlinedButton], [ElevatedButton] /// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton]
/// * FAB: [FloatingActionButton] /// * FAB: [FloatingActionButton]
/// * Extended FAB: [FloatingActionButton.extended] /// * Extended FAB: [FloatingActionButton.extended]
/// * Cards: [Card] /// * Cards: [Card]
...@@ -1464,6 +1470,10 @@ class ThemeData with Diagnosticable { ...@@ -1464,6 +1470,10 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the visual properties of [ExpansionTile]s. /// A theme for customizing the visual properties of [ExpansionTile]s.
final ExpansionTileThemeData expansionTileTheme; final ExpansionTileThemeData expansionTileTheme;
/// A theme for customizing the appearance and internal layout of
/// [FilledButton]s.
final FilledButtonThemeData filledButtonTheme;
/// A theme for customizing the shape, elevation, and color of a /// A theme for customizing the shape, elevation, and color of a
/// [FloatingActionButton]. /// [FloatingActionButton].
final FloatingActionButtonThemeData floatingActionButtonTheme; final FloatingActionButtonThemeData floatingActionButtonTheme;
...@@ -1739,6 +1749,7 @@ class ThemeData with Diagnosticable { ...@@ -1739,6 +1749,7 @@ class ThemeData with Diagnosticable {
DrawerThemeData? drawerTheme, DrawerThemeData? drawerTheme,
ElevatedButtonThemeData? elevatedButtonTheme, ElevatedButtonThemeData? elevatedButtonTheme,
ExpansionTileThemeData? expansionTileTheme, ExpansionTileThemeData? expansionTileTheme,
FilledButtonThemeData? filledButtonTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme, IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme, ListTileThemeData? listTileTheme,
...@@ -1884,6 +1895,7 @@ class ThemeData with Diagnosticable { ...@@ -1884,6 +1895,7 @@ class ThemeData with Diagnosticable {
drawerTheme: drawerTheme ?? this.drawerTheme, drawerTheme: drawerTheme ?? this.drawerTheme,
elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme, elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme,
expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme, expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme,
filledButtonTheme: filledButtonTheme ?? this.filledButtonTheme,
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme, iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme,
listTileTheme: listTileTheme ?? this.listTileTheme, listTileTheme: listTileTheme ?? this.listTileTheme,
...@@ -2083,6 +2095,7 @@ class ThemeData with Diagnosticable { ...@@ -2083,6 +2095,7 @@ class ThemeData with Diagnosticable {
drawerTheme: DrawerThemeData.lerp(a.drawerTheme, b.drawerTheme, t)!, drawerTheme: DrawerThemeData.lerp(a.drawerTheme, b.drawerTheme, t)!,
elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t)!, elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t)!,
expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!, expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!,
filledButtonTheme: FilledButtonThemeData.lerp(a.filledButtonTheme, b.filledButtonTheme, t)!,
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!, floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!, iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!,
listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!, listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!,
...@@ -2184,6 +2197,7 @@ class ThemeData with Diagnosticable { ...@@ -2184,6 +2197,7 @@ class ThemeData with Diagnosticable {
other.drawerTheme == drawerTheme && other.drawerTheme == drawerTheme &&
other.elevatedButtonTheme == elevatedButtonTheme && other.elevatedButtonTheme == elevatedButtonTheme &&
other.expansionTileTheme == expansionTileTheme && other.expansionTileTheme == expansionTileTheme &&
other.filledButtonTheme == filledButtonTheme &&
other.floatingActionButtonTheme == floatingActionButtonTheme && other.floatingActionButtonTheme == floatingActionButtonTheme &&
other.iconButtonTheme == iconButtonTheme && other.iconButtonTheme == iconButtonTheme &&
other.listTileTheme == listTileTheme && other.listTileTheme == listTileTheme &&
...@@ -2282,6 +2296,7 @@ class ThemeData with Diagnosticable { ...@@ -2282,6 +2296,7 @@ class ThemeData with Diagnosticable {
drawerTheme, drawerTheme,
elevatedButtonTheme, elevatedButtonTheme,
expansionTileTheme, expansionTileTheme,
filledButtonTheme,
floatingActionButtonTheme, floatingActionButtonTheme,
iconButtonTheme, iconButtonTheme,
listTileTheme, listTileTheme,
...@@ -2382,6 +2397,7 @@ class ThemeData with Diagnosticable { ...@@ -2382,6 +2397,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<DrawerThemeData>('drawerTheme', drawerTheme, defaultValue: defaultData.drawerTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<DrawerThemeData>('drawerTheme', drawerTheme, defaultValue: defaultData.drawerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ElevatedButtonThemeData>('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<ElevatedButtonThemeData>('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<ExpansionTileThemeData>('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FilledButtonThemeData>('filledButtonTheme', filledButtonTheme, defaultValue: defaultData.filledButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug));
......
// Copyright 2014 The Flutter 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('FilledButton, FilledButton.icon defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
// Enabled FilledButton
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Center(
child: FilledButton(
onPressed: () { },
child: const Text('button'),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
);
Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.primary);
expect(material.elevation, 0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onPrimary);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
final Offset center = tester.getCenter(find.byType(FilledButton));
await tester.startGesture(center);
await tester.pump(); // start the splash animation
await tester.pump(const Duration(milliseconds: 100)); // splash is underway
// Enabled FilledButton.icon
final Key iconButtonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Center(
child: FilledButton.icon(
key: iconButtonKey,
onPressed: () { },
icon: const Icon(Icons.add),
label: const Text('label'),
),
),
),
);
final Finder iconButtonMaterial = find.descendant(
of: find.byKey(iconButtonKey),
matching: find.byType(Material),
);
material = tester.widget<Material>(iconButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.primary);
expect(material.elevation, 0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onPrimary);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
// Disabled FilledButton
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: const Center(
child: FilledButton(
onPressed: null,
child: Text('button'),
),
),
),
);
// Finish the elevation animation, final background color change.
await tester.pumpAndSettle();
material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.onSurface.withOpacity(0.12));
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
});
testWidgets('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
// Enabled FilledButton
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Center(
child: FilledButton.tonal(
onPressed: () { },
child: const Text('button'),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
);
Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.secondaryContainer);
expect(material.elevation, 0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onSecondaryContainer);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
final Offset center = tester.getCenter(find.byType(FilledButton));
await tester.startGesture(center);
await tester.pump(); // start the splash animation
await tester.pump(const Duration(milliseconds: 100)); // splash is underway
// Enabled FilledButton.tonalIcon
final Key iconButtonKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Center(
child: FilledButton.tonalIcon(
key: iconButtonKey,
onPressed: () { },
icon: const Icon(Icons.add),
label: const Text('label'),
),
),
),
);
final Finder iconButtonMaterial = find.descendant(
of: find.byKey(iconButtonKey),
matching: find.byType(Material),
);
material = tester.widget<Material>(iconButtonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.secondaryContainer);
expect(material.elevation, 0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onSecondaryContainer);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
// Disabled FilledButton
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: const Center(
child: FilledButton.tonal(
onPressed: null,
child: Text('button'),
),
),
),
);
// Finish the elevation animation, final background color change.
await tester.pumpAndSettle();
material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, colorScheme.onSurface.withOpacity(0.12));
expect(material.elevation, 0.0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.button);
});
testWidgets('Default FilledButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scaffold(
body: Center(
child: FilledButton(
onPressed: () { },
focusNode: focusNode,
child: const Text('FilledButton'),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(FilledButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
);
testWidgets('FilledButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FilledButtonTheme(
data: FilledButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return FilledButton(
onPressed: () {},
focusNode: focusNode,
child: const Text('FilledButton'),
);
},
),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('FilledButton')).text.style!.color!;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(FilledButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// 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(textColor(), pressedColor);
});
testWidgets('FilledButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FilledButtonTheme(
data: FilledButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
),
child: Builder(
builder: (BuildContext context) {
return FilledButton.icon(
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('FilledButton'),
onPressed: () {},
focusNode: focusNode,
);
},
),
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color!;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// 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(iconColor(), pressedColor);
});
testWidgets('FilledButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {
bool wasPressed;
Finder filledButton;
Widget buildFrame({ VoidCallback? onPressed, VoidCallback? onLongPress }) {
return Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
onPressed: onPressed,
onLongPress: onLongPress,
child: const Text('button'),
),
);
}
// onPressed not null, onLongPress null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onPressed: () { wasPressed = true; }),
);
filledButton = find.byType(FilledButton);
expect(tester.widget<FilledButton>(filledButton).enabled, true);
await tester.tap(filledButton);
expect(wasPressed, true);
// onPressed null, onLongPress not null.
wasPressed = false;
await tester.pumpWidget(
buildFrame(onLongPress: () { wasPressed = true; }),
);
filledButton = find.byType(FilledButton);
expect(tester.widget<FilledButton>(filledButton).enabled, true);
await tester.longPress(filledButton);
expect(wasPressed, true);
// onPressed null, onLongPress null.
await tester.pumpWidget(
buildFrame(),
);
filledButton = find.byType(FilledButton);
expect(tester.widget<FilledButton>(filledButton).enabled, false);
});
testWidgets('FilledButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {
bool didPressButton = false;
bool didLongPressButton = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
onPressed: () {
didPressButton = true;
},
onLongPress: () {
didLongPressButton = true;
},
child: const Text('button'),
),
),
);
final Finder filledButton = find.byType(FilledButton);
expect(tester.widget<FilledButton>(filledButton).enabled, true);
expect(didPressButton, isFalse);
await tester.tap(filledButton);
expect(didPressButton, isTrue);
expect(didLongPressButton, isFalse);
await tester.longPress(filledButton);
expect(didLongPressButton, isTrue);
});
testWidgets("FilledButton response doesn't hover when disabled", (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'FilledButton Focus');
final GlobalKey childKey = GlobalKey();
bool hovering = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: FilledButton(
autofocus: true,
onPressed: () {},
onLongPress: () {},
onHover: (bool value) { hovering = value; },
focusNode: focusNode,
child: SizedBox(key: childKey),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
await tester.pumpAndSettle();
expect(hovering, isTrue);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: FilledButton(
focusNode: focusNode,
onHover: (bool value) { hovering = value; },
onPressed: null,
child: SizedBox(key: childKey),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets('disabled and hovered FilledButton responds to mouse-exit', (WidgetTester tester) async {
int onHoverCount = 0;
late bool hover;
Widget buildFrame({ required bool enabled }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 100,
height: 100,
child: FilledButton(
onPressed: enabled ? () { } : null,
onHover: (bool value) {
onHoverCount += 1;
hover = value;
},
child: const Text('FilledButton'),
),
),
),
);
}
await tester.pumpWidget(buildFrame(enabled: true));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(FilledButton)));
await tester.pumpAndSettle();
expect(onHoverCount, 1);
expect(hover, true);
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
await gesture.moveTo(Offset.zero);
// Even though the FilledButton has been disabled, the mouse-exit still
// causes onHover(false) to be called.
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(FilledButton)));
await tester.pumpAndSettle();
// We no longer see hover events because the FilledButton is disabled
// and it's no longer in the "hovering" state.
expect(onHoverCount, 2);
expect(hover, false);
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
// The FilledButton was enabled while it contained the mouse, however
// we do not call onHover() because it may call setState().
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(FilledButton)) - const Offset(1, 1));
await tester.pumpAndSettle();
// Moving the mouse a little within the FilledButton doesn't change anything.
expect(onHoverCount, 2);
expect(hover, false);
});
testWidgets('Can set FilledButton focus and Can set unFocus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'FilledButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: () { },
child: const SizedBox(),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
node.unfocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('When FilledButton disable, Can not set FilledButton focus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'FilledButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: null,
child: const SizedBox(),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('Does FilledButton work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
return states.contains(MaterialState.hovered) ? hoverColor : null;
}),
),
onPressed: () { },
child: const Text('button'),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(FilledButton)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
});
testWidgets('Does FilledButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
final FocusNode focusNode = FocusNode(debugLabel: 'FilledButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
focusNode.requestFocus();
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does FilledButton work with autofocus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
Color? getOverlayColor(Set<MaterialState> states) {
return states.contains(MaterialState.focused) ? focusColor : null;
}
final FocusNode focusNode = FocusNode(debugLabel: 'FilledButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
autofocus: true,
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(getOverlayColor),
),
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Does FilledButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FilledButton(
style: const ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the semantics tree's rect and transform
// match the original version of this test.
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
),
onPressed: () { },
child: const Text('ABC'),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('FilledButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
const ButtonStyle style = ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
);
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Theme(
data: ThemeData(materialTapTargetSize: tapTargetSize),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FilledButton(
key: key,
style: style,
child: const SizedBox(width: 50.0, height: 8.0),
onPressed: () { },
),
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0));
});
testWidgets('FilledButton has no clip by default', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
onPressed: () { /* to make sure the button is enabled */ },
child: const Text('button'),
),
),
);
expect(
tester.renderObject(find.byType(FilledButton)),
paintsExactlyCountTimes(#clipPath, 0),
);
});
testWidgets('FilledButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
const Key childKey = Key('test child');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return tester.pumpWidget(
MaterialApp(
// Test was setup using fonts from Material 2, so make sure we always
// test against englishLike2014.
theme: ThemeData(textTheme: Typography.englishLike2014),
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: FilledButton(
style: ButtonStyle(
visualDensity: visualDensity,
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the corresponding button size matches
// the original version of this test.
minimumSize: const MaterialStatePropertyAll<Size>(Size(88, 36)),
),
key: key,
onPressed: () {},
child: useText
? const Text('Text', key: childKey)
: Container(key: childKey, width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(VisualDensity.standard);
final RenderBox box = tester.renderObject(find.byKey(key));
Rect childRect = tester.getRect(find.byKey(childKey));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(156, 124)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(132, 100)));
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
await buildTest(VisualDensity.standard, useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(88, 48)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(112, 60)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
childRect = tester.getRect(find.byKey(childKey));
expect(box.size, equals(const Size(88, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
testWidgets('FilledButton.icon responds to applied padding', (WidgetTester tester) async {
const Key buttonKey = Key('test');
const Key labelKey = Key('label');
await tester.pumpWidget(
// When textDirection is set to TextDirection.ltr, the label appears on the
// right side of the icon. This is important in determining whether the
// horizontal padding is applied correctly later on
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FilledButton.icon(
key: buttonKey,
style: const ButtonStyle(
padding: MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.fromLTRB(16, 5, 10, 12)),
),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text(
'Hello',
key: labelKey,
),
),
),
),
);
final Rect paddingRect = tester.getRect(find.byType(Padding));
final Rect labelRect = tester.getRect(find.byKey(labelKey));
final Rect iconRect = tester.getRect(find.byType(Icon));
// The right padding should be applied on the right of the label, whereas the
// left padding should be applied on the left side of the icon.
expect(paddingRect.right, labelRect.right + 10);
expect(paddingRect.left, iconRect.left - 16);
// Use the taller widget to check the top and bottom padding.
final Rect tallerWidget = iconRect.height > labelRect.height ? iconRect : labelRect;
expect(paddingRect.top, tallerWidget.top - 5);
expect(paddingRect.bottom, tallerWidget.bottom + 12);
});
group('Default FilledButton padding for textScaleFactor, textDirection', () {
const ValueKey<String> buttonKey = ValueKey<String>('button');
const ValueKey<String> labelKey = ValueKey<String>('label');
const ValueKey<String> iconKey = ValueKey<String>('icon');
const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
const List<TextDirection> textDirectionOptions = <TextDirection>[TextDirection.ltr, TextDirection.rtl];
const List<Widget?> iconOptions = <Widget?>[null, Icon(Icons.add, size: 18, key: iconKey)];
// Expected values for each textScaleFactor.
final Map<double, double> paddingWithoutIconStart = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithoutIconEnd = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithIconStart = <double, double>{
0.5: 12,
1: 12,
1.25: 11,
1.5: 10,
2: 8,
2.5: 8,
3: 8,
4: 8,
};
final Map<double, double> paddingWithIconEnd = <double, double>{
0.5: 16,
1: 16,
1.25: 14,
1.5: 12,
2: 8,
2.5: 6,
3: 4,
4: 4,
};
final Map<double, double> paddingWithIconGap = <double, double>{
0.5: 8,
1: 8,
1.25: 7,
1.5: 6,
2: 4,
2.5: 4,
3: 4,
4: 4,
};
Rect globalBounds(RenderBox renderBox) {
final Offset topLeft = renderBox.localToGlobal(Offset.zero);
return topLeft & renderBox.size;
}
/// Computes the padding between two [Rect]s, one inside the other.
EdgeInsets paddingBetween({ required Rect parent, required Rect child }) {
assert (parent.intersect(child) == child);
return EdgeInsets.fromLTRB(
child.left - parent.left,
child.top - parent.top,
parent.right - child.right,
parent.bottom - child.bottom,
);
}
for (final double textScaleFactor in textScaleFactorOptions) {
for (final TextDirection textDirection in textDirectionOptions) {
for (final Widget? icon in iconOptions) {
final String testName = <String>[
'FilledButton, text scale $textScaleFactor',
if (icon != null)
'with icon',
if (textDirection == TextDirection.rtl)
'RTL',
].join(', ');
testWidgets(testName, (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
// Force Material 2 defaults for the typography and size
// default values as the test was designed against these settings.
textTheme: Typography.englishLike2014,
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(minimumSize: const Size(64, 36)),
),
),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: textScaleFactor,
),
child: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Center(
child: icon == null
? FilledButton(
key: buttonKey,
onPressed: () {},
child: const Text('button', key: labelKey),
)
: FilledButton.icon(
key: buttonKey,
onPressed: () {},
icon: icon,
label: const Text('button', key: labelKey),
),
),
),
),
);
},
),
),
);
final Element paddingElement = tester.element(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byType(Padding),
),
);
expect(Directionality.of(paddingElement), textDirection);
final Padding paddingWidget = paddingElement.widget as Padding;
// Compute expected padding, and check.
final double expectedStart = icon != null
? paddingWithIconStart[textScaleFactor]!
: paddingWithoutIconStart[textScaleFactor]!;
final double expectedEnd = icon != null
? paddingWithIconEnd[textScaleFactor]!
: paddingWithoutIconEnd[textScaleFactor]!;
final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(expectedStart, 0, expectedEnd, 0)
.resolve(textDirection);
expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
// Measure padding in terms of the difference between the button and its label child
// and check that.
final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
final Rect labelBounds = globalBounds(labelRenderBox);
final RenderBox? iconRenderBox = icon == null ? null : tester.renderObject<RenderBox>(find.byKey(iconKey));
final Rect? iconBounds = icon == null ? null : globalBounds(iconRenderBox!);
final Rect childBounds = icon == null ? labelBounds : labelBounds.expandToInclude(iconBounds!);
// We measure the `InkResponse` descendant of the button
// element, because the button has a larger `RenderBox`
// which accommodates the minimum tap target with a height
// of 48.
final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(buttonKey),
matching: find.byWidgetPredicate(
(Widget widget) => widget is InkResponse,
),
),
);
final Rect buttonBounds = globalBounds(buttonRenderBox);
final EdgeInsets visuallyMeasuredPadding = paddingBetween(
parent: buttonBounds,
child: childBounds,
);
// Since there is a requirement of a minimum width of 64
// and a minimum height of 36 on material buttons, the visual
// padding of smaller buttons may not match their settings.
// Therefore, we only test buttons that are large enough.
if (buttonBounds.width > 64) {
expect(
visuallyMeasuredPadding.left,
expectedPadding.left,
);
expect(
visuallyMeasuredPadding.right,
expectedPadding.right,
);
}
if (buttonBounds.height > 36) {
expect(
visuallyMeasuredPadding.top,
expectedPadding.top,
);
expect(
visuallyMeasuredPadding.bottom,
expectedPadding.bottom,
);
}
// Check the gap between the icon and the label
if (icon != null) {
final double gapWidth = textDirection == TextDirection.ltr
? labelBounds.left - iconBounds!.right
: iconBounds!.left - labelBounds.right;
expect(gapWidth, paddingWithIconGap[textScaleFactor]);
}
// Check the text's height - should be consistent with the textScaleFactor.
final RenderBox textRenderObject = tester.renderObject<RenderBox>(
find.descendant(
of: find.byKey(labelKey),
matching: find.byElementPredicate(
(Element element) => element.widget is RichText,
),
),
);
final double textHeight = textRenderObject.paintBounds.size.height;
final double expectedTextHeight = 14 * textScaleFactor;
expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
});
}
}
}
});
testWidgets('Override FilledButton default padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 2,
),
child: Scaffold(
body: Center(
child: FilledButton(
style: FilledButton.styleFrom(padding: const EdgeInsets.all(22)),
onPressed: () {},
child: const Text('FilledButton'),
),
),
),
);
},
),
),
);
final Padding paddingWidget = tester.widget<Padding>(
find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget.padding, const EdgeInsets.all(22));
});
testWidgets('By default, FilledButton shape outline is defined by shape.side', (WidgetTester tester) async {
const Color borderColor = Color(0xff4caf50);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(colorScheme: const ColorScheme.light(), textTheme: Typography.englishLike2014),
home: Center(
child: FilledButton(
style: FilledButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
side: BorderSide(width: 10, color: borderColor),
),
minimumSize: const Size(64, 36),
),
onPressed: () {},
child: const Text('button'),
),
),
),
);
expect(find.byType(FilledButton), paints ..drrect(
// Outer and inner rect that give the outline a width of 10.
outer: RRect.fromLTRBR(0.0, 0.0, 116.0, 36.0, const Radius.circular(16)),
inner: RRect.fromLTRBR(10.0, 10.0, 106.0, 26.0, const Radius.circular(16 - 10)),
color: borderColor)
);
});
testWidgets('Fixed size FilledButtons', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FilledButton(
style: FilledButton.styleFrom(fixedSize: const Size(100, 100)),
onPressed: () {},
child: const Text('100x100'),
),
FilledButton(
style: FilledButton.styleFrom(fixedSize: const Size.fromWidth(200)),
onPressed: () {},
child: const Text('200xh'),
),
FilledButton(
style: FilledButton.styleFrom(fixedSize: const Size.fromHeight(200)),
onPressed: () {},
child: const Text('wx200'),
),
],
),
),
),
);
expect(tester.getSize(find.widgetWithText(FilledButton, '100x100')), const Size(100, 100));
expect(tester.getSize(find.widgetWithText(FilledButton, '200xh')).width, 200);
expect(tester.getSize(find.widgetWithText(FilledButton, 'wx200')).height, 200);
});
testWidgets('FilledButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async {
Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
style: FilledButton.styleFrom(
splashFactory: splashFactory,
),
onPressed: () { },
child: const Text('test'),
),
),
),
);
}
// NoSplash.splashFactory, no splash circles drawn
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
await gesture.up();
await tester.pumpAndSettle();
}
// InkRipple.splashFactory, one splash circle drawn.
await tester.pumpWidget(buildFrame(splashFactory: InkRipple.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
await gesture.up();
await tester.pumpAndSettle();
}
});
testWidgets('FilledButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Center(
child: FilledButton(
onPressed: () { },
child: const Text('button'),
),
),
),
);
final InkWell buttonInkWell = tester.widget<InkWell>(find.descendant(
of: find.byType(FilledButton),
matching: find.byType(InkWell),
));
if (debugDefaultTargetPlatformOverride! == TargetPlatform.android && !kIsWeb) {
expect(buttonInkWell.splashFactory, equals(InkSparkle.splashFactory));
} else {
expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory));
}
}, variant: TargetPlatformVariant.all());
testWidgets('FilledButton.icon does not overflow', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/77815
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
child: FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text( // Much wider than 200
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut a euismod nibh. Morbi laoreet purus.',
),
),
),
),
),
);
expect(tester.takeException(), null);
});
testWidgets('FilledButton.icon icon,label layout', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
final Key iconKey = UniqueKey();
final Key labelKey = UniqueKey();
final ButtonStyle style = FilledButton.styleFrom(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.standard, // dx=0, dy=0
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
child: FilledButton.icon(
key: buttonKey,
style: style,
onPressed: () {},
icon: SizedBox(key: iconKey, width: 50, height: 100),
label: SizedBox(key: labelKey, width: 50, height: 100),
),
),
),
),
);
// The button's label and icon are separated by a gap of 8:
// 46 [icon 50] 8 [label 50] 46
// The overall button width is 200. So:
// icon.x = 46
// label.x = 46 + 50 + 8 = 104
expect(tester.getRect(find.byKey(buttonKey)), const Rect.fromLTRB(0.0, 0.0, 200.0, 100.0));
expect(tester.getRect(find.byKey(iconKey)), const Rect.fromLTRB(46.0, 0.0, 96.0, 100.0));
expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0));
});
testWidgets('FilledButton maximumSize', (WidgetTester tester) async {
final Key key0 = UniqueKey();
final Key key1 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(textTheme: Typography.englishLike2014),
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FilledButton(
key: key0,
style: TextButton.styleFrom(
minimumSize: const Size(24, 36),
maximumSize: const Size.fromWidth(64),
),
onPressed: () { },
child: const Text('A B C D E F G H I J K L M N O P'),
),
FilledButton.icon(
key: key1,
style: TextButton.styleFrom(
minimumSize: const Size(24, 36),
maximumSize: const Size.fromWidth(104),
),
onPressed: () {},
icon: Container(color: Colors.red, width: 32, height: 32),
label: const Text('A B C D E F G H I J K L M N O P'),
),
],
),
),
),
),
);
expect(tester.getSize(find.byKey(key0)), const Size(64.0, 224.0));
expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0));
});
testWidgets('Fixed size FilledButton, same as minimumSize == maximumSize', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FilledButton(
style: FilledButton.styleFrom(fixedSize: const Size(200, 200)),
onPressed: () { },
child: const Text('200x200'),
),
FilledButton(
style: FilledButton.styleFrom(
minimumSize: const Size(200, 200),
maximumSize: const Size(200, 200),
),
onPressed: () { },
child: const Text('200,200'),
),
],
),
),
),
);
expect(tester.getSize(find.widgetWithText(FilledButton, '200x200')), const Size(200, 200));
expect(tester.getSize(find.widgetWithText(FilledButton, '200,200')), const Size(200, 200));
});
testWidgets('FilledButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FilledButton(
style: FilledButton.styleFrom(
enabledMouseCursor: SystemMouseCursors.text,
disabledMouseCursor: SystemMouseCursors.grab,
),
onPressed: () {},
child: const Text('button'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test cursor when disabled
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FilledButton(
style: FilledButton.styleFrom(
enabledMouseCursor: SystemMouseCursors.text,
disabledMouseCursor: SystemMouseCursors.grab,
),
onPressed: null,
child: const Text('button'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FilledButton(
onPressed: () {},
child: const Text('button'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FilledButton(
onPressed: null,
child: Text('button'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('Ink Response shape matches Material shape', (WidgetTester tester) async {
Widget buildFrame({BorderSide? side}) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
style: FilledButton.styleFrom(
side: side,
shape: const RoundedRectangleBorder(
side: BorderSide(
color: Color(0xff0000ff),
width: 0,
),
),
),
onPressed: () { },
child: const Text('FilledButton'),
),
),
),
);
}
const BorderSide borderSide = BorderSide(width: 10, color: Color(0xff00ff00));
await tester.pumpWidget(buildFrame(side: borderSide));
expect(
tester.widget<InkWell>(find.byType(InkWell)).customBorder,
const RoundedRectangleBorder(side: borderSide),
);
await tester.pumpWidget(buildFrame());
await tester.pumpAndSettle();
expect(
tester.widget<InkWell>(find.byType(InkWell)).customBorder,
const RoundedRectangleBorder(
side: BorderSide(
color: Color(0xff0000ff),
width: 0.0,
),
),
);
});
testWidgets('FilledButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: FilledButton(
style: FilledButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.purple,
),
onPressed: () {},
child: const Text('button'),
),
),
),
);
final Material material = tester.widget<Material>(find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
));
expect(material.color, Colors.purple);
expect(material.textStyle!.color, Colors.white);
});
testWidgets('FilledButton statesController', (WidgetTester tester) async {
int count = 0;
void valueChanged() {
count += 1;
}
final MaterialStatesController controller = MaterialStatesController();
controller.addListener(valueChanged);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: FilledButton(
statesController: controller,
onPressed: () { },
child: const Text('button'),
),
),
),
);
expect(controller.value, <MaterialState>{});
expect(count, 0);
final Offset center = tester.getCenter(find.byType(FilledButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered});
expect(count, 1);
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{});
expect(count, 2);
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered});
expect(count, 3);
await gesture.down(center);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.pressed});
expect(count, 4);
await gesture.up();
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered});
expect(count, 5);
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{});
expect(count, 6);
await gesture.down(center);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.pressed});
expect(count, 8); // adds hovered and pressed - two changes
// If the button is rebuilt disabled, then the pressed state is
// removed.
await tester.pumpWidget(
MaterialApp(
home: Center(
child: FilledButton(
statesController: controller,
onPressed: null,
child: const Text('button'),
),
),
),
);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.hovered, MaterialState.disabled});
expect(count, 10); // removes pressed and adds disabled - two changes
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
expect(controller.value, <MaterialState>{MaterialState.disabled});
expect(count, 11);
await gesture.removePointer();
});
testWidgets('Disabled FilledButton statesController', (WidgetTester tester) async {
int count = 0;
void valueChanged() {
count += 1;
}
final MaterialStatesController controller = MaterialStatesController();
controller.addListener(valueChanged);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: FilledButton(
statesController: controller,
onPressed: null,
child: const Text('button'),
),
),
),
);
expect(controller.value, <MaterialState>{MaterialState.disabled});
expect(count, 1);
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style!;
}
// Copyright 2014 The Flutter 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_test/flutter_test.dart';
void main() {
testWidgets('Passing no FilledButtonTheme returns defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme),
home: Scaffold(
body: Center(
child: FilledButton(
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
);
final Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, colorScheme.primary);
expect(material.elevation, 0);
expect(material.shadowColor, const Color(0xff000000));
expect(material.shape, const StadiumBorder());
expect(material.textStyle!.color, colorScheme.onPrimary);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
});
group('[Theme, TextTheme, FilledButton style overrides]', () {
const Color foregroundColor = Color(0xff000001);
const Color backgroundColor = Color(0xff000002);
const Color disabledForegroundColor = Color(0xff000003);
const Color disabledBackgroundColor = Color(0xff000004);
const Color shadowColor = Color(0xff000005);
const double elevation = 1;
const TextStyle textStyle = TextStyle(fontSize: 12.0);
const EdgeInsets padding = EdgeInsets.all(3);
const Size minimumSize = Size(200, 200);
const BorderSide side = BorderSide(color: Colors.green, width: 2);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(2)));
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
const Duration animationDuration = Duration(milliseconds: 25);
const bool enableFeedback = false;
const AlignmentGeometry alignment = Alignment.centerLeft;
final ButtonStyle style = FilledButton.styleFrom(
foregroundColor: foregroundColor,
backgroundColor: backgroundColor,
disabledForegroundColor: disabledForegroundColor,
disabledBackgroundColor: disabledBackgroundColor,
shadowColor: shadowColor,
elevation: elevation,
textStyle: textStyle,
padding: padding,
minimumSize: minimumSize,
side: side,
shape: shape,
enabledMouseCursor: enabledMouseCursor,
disabledMouseCursor: disabledMouseCursor,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
);
Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) {
final Widget child = Builder(
builder: (BuildContext context) {
return FilledButton(
style: buttonStyle,
onPressed: () { },
child: const Text('button'),
);
},
);
return MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith(
filledButtonTheme: FilledButtonThemeData(style: overallStyle),
),
home: Scaffold(
body: Center(
// If the FilledButtonTheme widget is present, it's used
// instead of the Theme's ThemeData.FilledButtonTheme.
child: themeStyle == null ? child : FilledButtonTheme(
data: FilledButtonThemeData(style: themeStyle),
child: child,
),
),
),
);
}
final Finder findMaterial = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
);
final Finder findInkWell = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(InkWell),
);
const Set<MaterialState> enabled = <MaterialState>{};
const Set<MaterialState> disabled = <MaterialState>{ MaterialState.disabled };
const Set<MaterialState> hovered = <MaterialState>{ MaterialState.hovered };
const Set<MaterialState> focused = <MaterialState>{ MaterialState.focused };
const Set<MaterialState> pressed = <MaterialState>{ MaterialState.pressed };
void checkButton(WidgetTester tester) {
final Material material = tester.widget<Material>(findMaterial);
final InkWell inkWell = tester.widget<InkWell>(findInkWell);
expect(material.textStyle!.color, foregroundColor);
expect(material.textStyle!.fontSize, 12);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.elevation, elevation);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor!, enabled), enabledMouseCursor);
expect(MaterialStateProperty.resolveAs<MouseCursor>(inkWell.mouseCursor!, disabled), disabledMouseCursor);
expect(inkWell.overlayColor!.resolve(hovered), foregroundColor.withOpacity(0.08));
expect(inkWell.overlayColor!.resolve(focused), foregroundColor.withOpacity(0.12));
expect(inkWell.overlayColor!.resolve(pressed), foregroundColor.withOpacity(0.12));
expect(inkWell.enableFeedback, enableFeedback);
expect(material.borderRadius, null);
expect(material.shape, shape);
expect(material.animationDuration, animationDuration);
expect(tester.getSize(find.byType(FilledButton)), const Size(200, 200));
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, alignment);
}
testWidgets('Button style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(themeStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallStyle: style));
await tester.pumpAndSettle();
checkButton(tester);
});
// Same as the previous tests with empty ButtonStyle's instead of null.
testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle()));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style));
await tester.pumpAndSettle(); // allow the animations to finish
checkButton(tester);
});
});
testWidgets('Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
Widget buildFrame({ Color? overallShadowColor, Color? themeShadowColor, Color? shadowColor }) {
return MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme).copyWith(
shadowColor: overallShadowColor,
),
home: Scaffold(
body: Center(
child: FilledButtonTheme(
data: FilledButtonThemeData(
style: FilledButton.styleFrom(
shadowColor: themeShadowColor,
),
),
child: Builder(
builder: (BuildContext context) {
return FilledButton(
style: FilledButton.styleFrom(
shadowColor: shadowColor,
),
onPressed: () { },
child: const Text('button'),
);
},
),
),
),
),
);
}
final Finder buttonMaterialFinder = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(Material),
);
await tester.pumpWidget(buildFrame());
Material material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, Colors.black); //default
await tester.pumpWidget(buildFrame(themeShadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(shadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(overallShadowColor: overriddenColor, themeShadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
await tester.pumpWidget(buildFrame(themeShadowColor: overriddenColor, shadowColor: shadowColor));
await tester.pumpAndSettle(); // theme animation
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
}
...@@ -695,6 +695,7 @@ void main() { ...@@ -695,6 +695,7 @@ void main() {
drawerTheme: const DrawerThemeData(), drawerTheme: const DrawerThemeData(),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)), elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(backgroundColor: Colors.green)),
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black), expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
filledButtonTheme: FilledButtonThemeData(style: FilledButton.styleFrom(foregroundColor: Colors.green)),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)), iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)),
listTileTheme: const ListTileThemeData(), listTileTheme: const ListTileThemeData(),
...@@ -808,6 +809,7 @@ void main() { ...@@ -808,6 +809,7 @@ void main() {
drawerTheme: const DrawerThemeData(), drawerTheme: const DrawerThemeData(),
elevatedButtonTheme: const ElevatedButtonThemeData(), elevatedButtonTheme: const ElevatedButtonThemeData(),
expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black), expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black),
filledButtonTheme: const FilledButtonThemeData(),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
iconButtonTheme: const IconButtonThemeData(), iconButtonTheme: const IconButtonThemeData(),
listTileTheme: const ListTileThemeData(), listTileTheme: const ListTileThemeData(),
...@@ -907,6 +909,7 @@ void main() { ...@@ -907,6 +909,7 @@ void main() {
drawerTheme: otherTheme.drawerTheme, drawerTheme: otherTheme.drawerTheme,
elevatedButtonTheme: otherTheme.elevatedButtonTheme, elevatedButtonTheme: otherTheme.elevatedButtonTheme,
expansionTileTheme: otherTheme.expansionTileTheme, expansionTileTheme: otherTheme.expansionTileTheme,
filledButtonTheme: otherTheme.filledButtonTheme,
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme, floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
iconButtonTheme: otherTheme.iconButtonTheme, iconButtonTheme: otherTheme.iconButtonTheme,
listTileTheme: otherTheme.listTileTheme, listTileTheme: otherTheme.listTileTheme,
...@@ -1005,6 +1008,7 @@ void main() { ...@@ -1005,6 +1008,7 @@ void main() {
expect(themeDataCopy.drawerTheme, equals(otherTheme.drawerTheme)); expect(themeDataCopy.drawerTheme, equals(otherTheme.drawerTheme));
expect(themeDataCopy.elevatedButtonTheme, equals(otherTheme.elevatedButtonTheme)); expect(themeDataCopy.elevatedButtonTheme, equals(otherTheme.elevatedButtonTheme));
expect(themeDataCopy.expansionTileTheme, equals(otherTheme.expansionTileTheme)); expect(themeDataCopy.expansionTileTheme, equals(otherTheme.expansionTileTheme));
expect(themeDataCopy.filledButtonTheme, equals(otherTheme.filledButtonTheme));
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme)); expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme)); expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme));
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme)); expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
...@@ -1140,6 +1144,7 @@ void main() { ...@@ -1140,6 +1144,7 @@ void main() {
'dividerTheme', 'dividerTheme',
'drawerTheme', 'drawerTheme',
'elevatedButtonTheme', 'elevatedButtonTheme',
'filledButtonTheme',
'floatingActionButtonTheme', 'floatingActionButtonTheme',
'iconButtonTheme', 'iconButtonTheme',
'listTileTheme', 'listTileTheme',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment