Unverified Commit ff6c8f5d authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#141818)

Fixes https://github.com/flutter/flutter/issues/139456, https://github.com/flutter/flutter/issues/130335, https://github.com/flutter/flutter/issues/89563.

Two new properties have been added to ButtonStyle to make it possible to insert arbitrary state-dependent widgets in a button's background or foreground. These properties can be specified for an individual button, using the style parameter, or for all buttons using a button theme's style parameter.

The new ButtonStyle properties are `backgroundBuilder` and `foregroundBuilder` and their (function) types are:

```dart
typedef ButtonLayerBuilder = Widget Function(
  BuildContext context,
  Set<MaterialState> states,
  Widget? child
);
```

The new builder functions are called whenever the button is built and the `states` parameter communicates the pressed/hovered/etc state fo the button.

## `backgroundBuilder`

Creates a widget that becomes the child of the button's Material and whose child is the rest of the button, including the button's `child` parameter.  By default the returned widget is clipped to the Material's ButtonStyle.shape.

The `backgroundBuilder` can be used to add a gradient to the button's background. Here's an example that creates a yellow/orange gradient background:

![opaque-gradient-bg](https://github.com/flutter/flutter/assets/1377460/80df8368-e7cf-49ef-aee7-2776a573644c)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Because the background widget becomes the child of the button's Material, if it's opaque (as it is in this case) then it obscures the overlay highlights which are painted on the button's Material. To ensure that the highlights show through one can decorate the background with an `Ink` widget.  This version also overrides the overlay color to be (shades of) red, because that makes the highlights look a little nicer with the yellow/orange background.

![ink-gradient-bg](https://github.com/flutter/flutter/assets/1377460/68a49733-f30e-44a1-a948-dc8cc95e1716)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Now the button's overlay highlights are painted on the Ink widget. An Ink widget isn't needed if the background is sufficiently translucent. This version of the example creates a translucent backround widget. 

![translucent-graident-bg](https://github.com/flutter/flutter/assets/1377460/3b016e1f-200a-4d07-8111-e20d29f18014)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [
            Colors.orange.withOpacity(0.5),
            Colors.yellow.withOpacity(0.5),
          ]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

One can also decorate the background with an image. In this example, the button's background is an burlap texture image. The foreground color has been changed to black to make the button's text a little clearer relative to the mottled brown backround.

![burlap-bg](https://github.com/flutter/flutter/assets/1377460/f2f61ab1-10d9-43a4-bd63-beecdce33b45)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.black,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(burlapUrl),
            fit: BoxFit.cover,
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The background widget can depend on the `states` parameter. In this example the blue/orange gradient flips horizontally when the button is hovered/pressed.

![gradient-flip](https://github.com/flutter/flutter/assets/1377460/c6c6fe26-ae47-445b-b82d-4605d9583bd8)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The preceeding examples have not included a BoxDecoration border because ButtonStyle already supports `ButtonStyle.shape` and `ButtonStyle.side` parameters that can be uesd to define state-dependent borders. Borders defined with the ButtonStyle side parameter match the button's shape. To add a border that changes color when the button is hovered or pressed, one must specify the side property using `copyWith`, since there's no `styleFrom` shorthand for this case.

![border-gradient-bg](https://github.com/flutter/flutter/assets/1377460/63cffcd3-0dcf-4eb1-aed5-d14adf1e57f6)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.indigo,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ).copyWith(
    side: MaterialStateProperty.resolveWith<BorderSide?>((Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return BorderSide(width: 3, color: Colors.yellow);
      }
      return null; // defer to the default
    }),
  ),
  child: Text('Text Button'),
)
```

Although all of the examples have created a ButtonStyle locally and only applied it to one button, they could have configured the `ThemeData.textButtonTheme` instead and applied the style to all TextButtons. And, of course, all of this works for all of the ButtonStyleButton classes, not just TextButton.

## `foregroundBuilder`

Creates a Widget that contains the button's child parameter. The returned widget is clipped by the button's [ButtonStyle.shape] inset by the button's [ButtonStyle.padding] and aligned by the button's [ButtonStyle.alignment].

The `foregroundBuilder` can be used to wrap the button's child, e.g. with a border or a `ShaderMask` or as a state-dependent substitute for the child.

This example adds a border that's just applied to the child. The border only appears when the button is hovered/pressed.

![border-fg](https://github.com/flutter/flutter/assets/1377460/687a3245-fe68-4983-a04e-5fcc77f8aa21)

```dart
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return DecoratedBox(
        decoration: BoxDecoration(
          border: states.contains(MaterialState.hovered)
            ? Border(bottom: BorderSide(color: colorScheme.primary))
            : Border(), // essentially "no border"
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The foregroundBuilder can be used with `ShaderMask` to change the way the button's child is rendered. In this example the ShaderMask's gradient causes the button's child to fade out on top.

![shader_mask_fg](https://github.com/flutter/flutter/assets/1377460/54010f24-e65d-4551-ae58-712135df3d8d)

```dart
ElevatedButton(
  onPressed: () { },
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return ShaderMask(
        shaderCallback: (Rect bounds) {
          return LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            colors: <Color>[
              colorScheme.primary,
              colorScheme.primaryContainer,
            ],
          ).createShader(bounds);
        },
        blendMode: BlendMode.srcATop,
        child: child,
      );
    },
  ),
  child:  const Text('Elevated Button'),
)
```

A commonly requested configuration for butttons has the developer provide images, one for pressed/hovered/normal state. You can use the foregroundBuilder to create a button that fades between a normal image and another image when the button is pressed. In this case the foregroundBuilder doesn't use the child it's passed, even though we've provided the required TextButton child parameter.

![image-button](https://github.com/flutter/flutter/assets/1377460/f5b1a22f-43ce-4be3-8e70-06de4c958380)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final String url = states.contains(MaterialState.pressed) ? smiley2Url : smiley1Url;
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```

In this example the button's default overlay appears when the button is hovered and pressed. Another image can be used to indicate the hovered state and the default overlay can be defeated by specifying `Colors.transparent` for the `overlayColor`:

![image-per-state](https://github.com/flutter/flutter/assets/1377460/7ab9da2f-f661-4374-b395-c2e0c7c4cf13)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.transparent,
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      String url = states.contains(MaterialState.hovered) ? smiley3Url : smiley1Url;
      if (states.contains(MaterialState.pressed)) {
        url = smiley2Url;
      }
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```
parent 5c9662e6
...@@ -374,7 +374,6 @@ final Set<String> _knownMissingTests = <String>{ ...@@ -374,7 +374,6 @@ final Set<String> _knownMissingTests = <String>{
'examples/api/test/material/checkbox/checkbox.1_test.dart', 'examples/api/test/material/checkbox/checkbox.1_test.dart',
'examples/api/test/material/checkbox/checkbox.0_test.dart', 'examples/api/test/material/checkbox/checkbox.0_test.dart',
'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart', 'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart',
'examples/api/test/material/text_button/text_button.0_test.dart',
'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', 'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart',
'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart',
......
// 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:io';
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/text_button/text_button.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
// The app being tested loads images via HTTP which the test
// framework defeats by default.
setUpAll(() {
HttpOverrides.global = null;
});
testWidgets('TextButtonExample smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const example.TextButtonExampleApp());
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'Enabled'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'Disabled'));
await tester.pumpAndSettle();
// TextButton.icon buttons are _TextButtonWithIcons rather than TextButtons.
// For the purposes of this test, just tapping in the right place is OK.
await tester.tap(find.text('TextButton.icon #1'));
await tester.pumpAndSettle();
await tester.tap(find.text('TextButton.icon #2'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #3'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #4'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #5'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #6'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #7'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(TextButton, 'TextButton #8'));
await tester.pumpAndSettle();
await tester.tap(find.byType(TextButton).last); // Smiley image button
await tester.pumpAndSettle();
await tester.tap(find.byType(Switch).at(0)); // Dark Mode Switch
await tester.pumpAndSettle();
await tester.tap(find.byType(Switch).at(1)); // RTL Text Switch
await tester.pumpAndSettle();
});
}
...@@ -16,6 +16,12 @@ import 'theme_data.dart'; ...@@ -16,6 +16,12 @@ import 'theme_data.dart';
// late BuildContext context; // late BuildContext context;
// typedef MyAppHome = Placeholder; // typedef MyAppHome = Placeholder;
/// The type for [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder].
///
/// The [states] parameter is the button's current pressed/hovered/etc state. The [child] is
/// typically a descendant of the returned widget.
typedef ButtonLayerBuilder = Widget Function(BuildContext context, Set<MaterialState> states, Widget? child);
/// The visual properties that most buttons have in common. /// The visual properties that most buttons have in common.
/// ///
/// Buttons and their themes have a ButtonStyle property which defines the visual /// Buttons and their themes have a ButtonStyle property which defines the visual
...@@ -162,6 +168,8 @@ class ButtonStyle with Diagnosticable { ...@@ -162,6 +168,8 @@ class ButtonStyle with Diagnosticable {
this.enableFeedback, this.enableFeedback,
this.alignment, this.alignment,
this.splashFactory, this.splashFactory,
this.backgroundBuilder,
this.foregroundBuilder,
}); });
/// The style for a button's [Text] widget descendants. /// The style for a button's [Text] widget descendants.
...@@ -315,6 +323,42 @@ class ButtonStyle with Diagnosticable { ...@@ -315,6 +323,42 @@ class ButtonStyle with Diagnosticable {
/// ``` /// ```
final InteractiveInkFeatureFactory? splashFactory; final InteractiveInkFeatureFactory? splashFactory;
/// Creates a widget that becomes the child of the button's [Material]
/// and whose child is the rest of the button, including the button's
/// `child` parameter.
///
/// The widget created by [backgroundBuilder] is constrained to be
/// the same size as the overall button and will appear behind the
/// button's child. The widget created by [foregroundBuilder] is
/// constrained to be the same size as the button's child, i.e. it's
/// inset by [ButtonStyle.padding] and aligned by the button's
/// [ButtonStyle.alignment].
///
/// By default the returned widget is clipped to the Material's [ButtonStyle.shape].
///
/// See also:
///
/// * [foregroundBuilder], to create a widget that's as big as the button's
/// child and is layered behind the child.
/// * [ButtonStyleButton.clipBehavior], for more information about
/// configuring clipping.
final ButtonLayerBuilder? backgroundBuilder;
/// Creates a Widget that contains the button's child parameter which is used
/// instead of the button's child.
///
/// The returned widget is clipped by the button's
/// [ButtonStyle.shape], inset by the button's [ButtonStyle.padding]
/// and aligned by the button's [ButtonStyle.alignment].
///
/// See also:
///
/// * [backgroundBuilder], to create a widget that's as big as the button and
/// is layered behind the button's child.
/// * [ButtonStyleButton.clipBehavior], for more information about
/// configuring clipping.
final ButtonLayerBuilder? foregroundBuilder;
/// Returns a copy of this ButtonStyle with the given fields replaced with /// Returns a copy of this ButtonStyle with the given fields replaced with
/// the new values. /// the new values.
ButtonStyle copyWith({ ButtonStyle copyWith({
...@@ -340,6 +384,8 @@ class ButtonStyle with Diagnosticable { ...@@ -340,6 +384,8 @@ class ButtonStyle with Diagnosticable {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
return ButtonStyle( return ButtonStyle(
textStyle: textStyle ?? this.textStyle, textStyle: textStyle ?? this.textStyle,
...@@ -364,6 +410,8 @@ class ButtonStyle with Diagnosticable { ...@@ -364,6 +410,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
alignment: alignment ?? this.alignment, alignment: alignment ?? this.alignment,
splashFactory: splashFactory ?? this.splashFactory, splashFactory: splashFactory ?? this.splashFactory,
backgroundBuilder: backgroundBuilder ?? this.backgroundBuilder,
foregroundBuilder: foregroundBuilder ?? this.foregroundBuilder,
); );
} }
...@@ -399,6 +447,8 @@ class ButtonStyle with Diagnosticable { ...@@ -399,6 +447,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: enableFeedback ?? style.enableFeedback, enableFeedback: enableFeedback ?? style.enableFeedback,
alignment: alignment ?? style.alignment, alignment: alignment ?? style.alignment,
splashFactory: splashFactory ?? style.splashFactory, splashFactory: splashFactory ?? style.splashFactory,
backgroundBuilder: backgroundBuilder ?? style.backgroundBuilder,
foregroundBuilder: foregroundBuilder ?? style.foregroundBuilder,
); );
} }
...@@ -427,6 +477,8 @@ class ButtonStyle with Diagnosticable { ...@@ -427,6 +477,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback, enableFeedback,
alignment, alignment,
splashFactory, splashFactory,
backgroundBuilder,
foregroundBuilder,
]; ];
return Object.hashAll(values); return Object.hashAll(values);
} }
...@@ -461,7 +513,9 @@ class ButtonStyle with Diagnosticable { ...@@ -461,7 +513,9 @@ class ButtonStyle with Diagnosticable {
&& other.animationDuration == animationDuration && other.animationDuration == animationDuration
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.alignment == alignment && other.alignment == alignment
&& other.splashFactory == splashFactory; && other.splashFactory == splashFactory
&& other.backgroundBuilder == backgroundBuilder
&& other.foregroundBuilder == foregroundBuilder;
} }
@override @override
...@@ -488,6 +542,8 @@ class ButtonStyle with Diagnosticable { ...@@ -488,6 +542,8 @@ class ButtonStyle with Diagnosticable {
properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null)); properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null)); properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
properties.add(DiagnosticsProperty<ButtonLayerBuilder>('backgroundBuilder', backgroundBuilder, defaultValue: null));
properties.add(DiagnosticsProperty<ButtonLayerBuilder>('foregroundBuilder', foregroundBuilder, defaultValue: null));
} }
/// Linearly interpolate between two [ButtonStyle]s. /// Linearly interpolate between two [ButtonStyle]s.
...@@ -518,6 +574,8 @@ class ButtonStyle with Diagnosticable { ...@@ -518,6 +574,8 @@ class ButtonStyle with Diagnosticable {
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory, splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory,
backgroundBuilder: t < 0.5 ? a?.backgroundBuilder : b?.backgroundBuilder,
foregroundBuilder: t < 0.5 ? a?.foregroundBuilder : b?.foregroundBuilder,
); );
} }
......
...@@ -89,8 +89,10 @@ abstract class ButtonStyleButton extends StatefulWidget { ...@@ -89,8 +89,10 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
/// ///
/// Defaults to [Clip.none]. /// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
final Clip clipBehavior; /// [ButtonStyle.foregroundBuilder] is specified. In those
/// cases the default is [Clip.antiAlias].
final Clip? clipBehavior;
/// {@macro flutter.widgets.Focus.focusNode} /// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode; final FocusNode? focusNode;
...@@ -318,6 +320,11 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -318,6 +320,11 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
final AlignmentGeometry? resolvedAlignment = effectiveValue((ButtonStyle? style) => style?.alignment); final AlignmentGeometry? resolvedAlignment = effectiveValue((ButtonStyle? style) => style?.alignment);
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment; final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue((ButtonStyle? style) => style?.splashFactory); final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue((ButtonStyle? style) => style?.splashFactory);
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue((ButtonStyle? style) => style?.backgroundBuilder);
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue((ButtonStyle? style) => style?.foregroundBuilder);
final Clip effectiveClipBehavior = widget.clipBehavior
?? ((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null ? Clip.antiAlias : Clip.none);
BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints( BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints(
BoxConstraints( BoxConstraints(
...@@ -384,6 +391,21 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -384,6 +391,21 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
elevation = resolvedElevation; elevation = resolvedElevation;
backgroundColor = resolvedBackgroundColor; backgroundColor = resolvedBackgroundColor;
Widget effectiveChild = Padding(
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: resolvedForegroundBuilder != null
? resolvedForegroundBuilder(context, statesController.value, widget.child)
: widget.child,
),
);
if (resolvedBackgroundBuilder != null) {
effectiveChild = resolvedBackgroundBuilder(context, statesController.value, effectiveChild);
}
final Widget result = ConstrainedBox( final Widget result = ConstrainedBox(
constraints: effectiveConstraints, constraints: effectiveConstraints,
child: Material( child: Material(
...@@ -395,7 +417,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -395,7 +417,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
surfaceTintColor: resolvedSurfaceTintColor, surfaceTintColor: resolvedSurfaceTintColor,
type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button, type: resolvedBackgroundColor == null ? MaterialType.transparency : MaterialType.button,
animationDuration: resolvedAnimationDuration, animationDuration: resolvedAnimationDuration,
clipBehavior: widget.clipBehavior, clipBehavior: effectiveClipBehavior,
child: InkWell( child: InkWell(
onTap: widget.onPressed, onTap: widget.onPressed,
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
...@@ -413,15 +435,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat ...@@ -413,15 +435,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
statesController: statesController, statesController: statesController,
child: IconTheme.merge( child: IconTheme.merge(
data: IconThemeData(color: resolvedIconColor ?? resolvedForegroundColor, size: resolvedIconSize), data: IconThemeData(color: resolvedIconColor ?? resolvedForegroundColor, size: resolvedIconSize),
child: Padding( child: effectiveChild,
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: widget.child,
),
),
), ),
), ),
), ),
......
...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';
import 'button_style.dart'; import 'button_style.dart';
import 'button_style_button.dart'; import 'button_style_button.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'elevated_button_theme.dart'; import 'elevated_button_theme.dart';
import 'ink_ripple.dart'; import 'ink_ripple.dart';
...@@ -70,7 +71,7 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -70,7 +71,7 @@ class ElevatedButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
required super.child, required super.child,
}); });
...@@ -100,19 +101,26 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -100,19 +101,26 @@ class ElevatedButton extends ButtonStyleButton {
/// ///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// The button's elevations are defined relative to the [elevation] /// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter /// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered /// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed. /// 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 /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
/// states. /// states.
...@@ -154,6 +162,9 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -154,6 +162,9 @@ class ElevatedButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -170,32 +181,40 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -170,32 +181,40 @@ class ElevatedButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? background = backgroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledBackground = disabledBackgroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? backgroundColorProp = (background == null && disabledBackground == null) (_, _) => _ElevatedButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _ElevatedButtonDefaultColor(background, disabledBackground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final Color? foreground = foregroundColor; (null, null) => null,
final Color? disabledForeground = disabledForegroundColor; (_, _) => _ElevatedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) };
? null final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _ElevatedButtonDefaultColor(foreground, disabledForeground); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _ElevatedButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _ElevatedButtonDefaultOverlay(foreground); final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
final MaterialStateProperty<double>? elevationValue = (elevation == null) (null, null) => null,
? null (_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
: _ElevatedButtonDefaultElevation(elevation); (_, _) => _ElevatedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
};
final MaterialStateProperty<double>? elevationValue = switch (elevation) {
null => null,
_ => _ElevatedButtonDefaultElevation(elevation),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle), textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: elevationValue, elevation: elevationValue,
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -210,6 +229,8 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -210,6 +229,8 @@ class ElevatedButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -251,7 +272,7 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -251,7 +272,7 @@ class ElevatedButton extends ButtonStyleButton {
/// * others - Theme.colorScheme.onPrimary /// * others - Theme.colorScheme.onPrimary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.onPrimary(0.08) /// * hovered - Theme.colorScheme.onPrimary(0.08)
/// * focused or pressed - Theme.colorScheme.onPrimary(0.24) /// * focused or pressed - Theme.colorScheme.onPrimary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` /// * `elevation`
/// * disabled - 0 /// * disabled - 0
...@@ -475,13 +496,12 @@ class _ElevatedButtonWithIcon extends ElevatedButton { ...@@ -475,13 +496,12 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -153,19 +153,23 @@ class FilledButton extends ButtonStyleButton { ...@@ -153,19 +153,23 @@ class FilledButton extends ButtonStyleButton {
/// A static convenience method that constructs a filled button /// A static convenience method that constructs a filled button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
/// ///
/// The [foregroundColor], and [disabledForegroundColor] colors are used to create a /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// [MaterialStateProperty] [ButtonStyle.foregroundColor] value. The /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// [backgroundColor] and [disabledBackgroundColor] are used to create a /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// [MaterialStateProperty] [ButtonStyle.backgroundColor] value. ///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
/// ///
/// The button's elevations are defined relative to the [elevation] /// The button's elevations are defined relative to the [elevation]
/// parameter. The disabled elevation is the same as the parameter /// parameter. The disabled elevation is the same as the parameter
/// value, [elevation] + 2 is used when the button is hovered /// value, [elevation] + 2 is used when the button is hovered
/// or focused, and elevation + 6 is used when the button is pressed. /// 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 /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
/// states. /// states.
...@@ -201,6 +205,9 @@ class FilledButton extends ButtonStyleButton { ...@@ -201,6 +205,9 @@ class FilledButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -217,29 +224,36 @@ class FilledButton extends ButtonStyleButton { ...@@ -217,29 +224,36 @@ class FilledButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final MaterialStateProperty<Color?>? backgroundColorProp = final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
(backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, _) => _FilledButtonDefaultColor(foregroundColor, disabledForegroundColor),
: _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor); };
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (_, _) => _FilledButtonDefaultColor(backgroundColor, disabledBackgroundColor),
(foreground == null && disabledForeground == null) };
? null final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _FilledButtonDefaultColor(foreground, disabledForeground); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _FilledButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _FilledButtonDefaultOverlay(foreground); final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _FilledButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _FilledButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle), textStyle: MaterialStatePropertyAll<TextStyle?>(textStyle),
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: ButtonStyleButton.allOrNull(elevation), elevation: ButtonStyleButton.allOrNull(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -254,6 +268,8 @@ class FilledButton extends ButtonStyleButton { ...@@ -254,6 +268,8 @@ class FilledButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -468,14 +484,13 @@ class _FilledButtonWithIcon extends FilledButton { ...@@ -468,14 +484,13 @@ class _FilledButtonWithIcon extends FilledButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style)
child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
_FilledButtonWithIcon.tonal({ _FilledButtonWithIcon.tonal({
...@@ -487,14 +502,13 @@ class _FilledButtonWithIcon extends FilledButton { ...@@ -487,14 +502,13 @@ class _FilledButtonWithIcon extends FilledButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super.tonal( }) : super.tonal(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none, child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style)
child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
@override @override
......
...@@ -168,6 +168,9 @@ abstract class MaterialStateColor extends Color implements MaterialStateProperty ...@@ -168,6 +168,9 @@ abstract class MaterialStateColor extends Color implements MaterialStateProperty
/// specified state. /// specified state.
@override @override
Color resolve(Set<MaterialState> states); Color resolve(Set<MaterialState> states);
/// A constant whose value is [Colors.transparent] for all states.
static const MaterialStateColor transparent = _MaterialStateColorTransparent();
} }
/// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>] /// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>]
...@@ -189,6 +192,13 @@ class _MaterialStateColor extends MaterialStateColor { ...@@ -189,6 +192,13 @@ class _MaterialStateColor extends MaterialStateColor {
Color resolve(Set<MaterialState> states) => _resolve(states); Color resolve(Set<MaterialState> states) => _resolve(states);
} }
class _MaterialStateColorTransparent extends MaterialStateColor {
const _MaterialStateColorTransparent() : super(0x00000000);
@override
Color resolve(Set<MaterialState> states) => const Color(0x00000000);
}
/// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which /// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which
/// represent the interactive state of a component. /// represent the interactive state of a component.
/// ///
......
...@@ -75,7 +75,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -75,7 +75,7 @@ class OutlinedButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
required super.child, required super.child,
}); });
...@@ -101,16 +101,22 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -101,16 +101,22 @@ class OutlinedButton extends ButtonStyleButton {
/// A static convenience method that constructs an outlined button /// A static convenience method that constructs an outlined button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
/// ///
///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor] /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor]. /// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
...@@ -141,6 +147,9 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -141,6 +147,9 @@ class OutlinedButton extends ButtonStyleButton {
Color? disabledBackgroundColor, Color? disabledBackgroundColor,
Color? shadowColor, Color? shadowColor,
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor,
Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -157,29 +166,36 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -157,29 +166,36 @@ class OutlinedButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) (_, _) => _OutlinedButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _OutlinedButtonDefaultColor(foreground, disabledForeground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, _) => _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor),
: disabledBackgroundColor == null };
? ButtonStyleButton.allOrNull<Color?>(backgroundColor) final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _OutlinedButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _OutlinedButtonDefaultOverlay(foreground); final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
(null, null) => null,
(_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
(_, _) => _OutlinedButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
};
final MaterialStateProperty<MouseCursor?> mouseCursor = _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _OutlinedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle), textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
elevation: ButtonStyleButton.allOrNull<double>(elevation), elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize), minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
...@@ -194,6 +210,8 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -194,6 +210,8 @@ class OutlinedButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -228,7 +246,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -228,7 +246,7 @@ class OutlinedButton extends ButtonStyleButton {
/// * disabled - Theme.colorScheme.onSurface(0.38) /// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary /// * others - Theme.colorScheme.primary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04) /// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12) /// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0 /// * `elevation` - 0
...@@ -311,9 +329,7 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -311,9 +329,7 @@ class OutlinedButton extends ButtonStyleButton {
padding: _scaledPadding(context), padding: _scaledPadding(context),
minimumSize: const Size(64, 36), minimumSize: const Size(64, 36),
maximumSize: Size.infinite, maximumSize: Size.infinite,
side: BorderSide( side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
enabledMouseCursor: SystemMouseCursors.click, enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.basic, disabledMouseCursor: SystemMouseCursors.basic,
...@@ -406,13 +422,12 @@ class _OutlinedButtonWithIcon extends OutlinedButton { ...@@ -406,13 +422,12 @@ class _OutlinedButtonWithIcon extends OutlinedButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _OutlinedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _OutlinedButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -50,8 +50,9 @@ import 'theme_data.dart'; ...@@ -50,8 +50,9 @@ import 'theme_data.dart';
/// button will be disabled, it will not react to touch. /// button will be disabled, it will not react to touch.
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// This sample shows how to render a disabled TextButton, an enabled TextButton /// This sample shows various ways to configure TextButtons, from the
/// and lastly a TextButton with gradient background. /// simplest default appearance to versions that don't resemble
/// Material Design at all.
/// ///
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart ** /// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
/// {@end-tool} /// {@end-tool}
...@@ -82,7 +83,7 @@ class TextButton extends ButtonStyleButton { ...@@ -82,7 +83,7 @@ class TextButton extends ButtonStyleButton {
super.style, super.style,
super.focusNode, super.focusNode,
super.autofocus = false, super.autofocus = false,
super.clipBehavior = Clip.none, super.clipBehavior,
super.statesController, super.statesController,
super.isSemanticButton, super.isSemanticButton,
required Widget super.child, required Widget super.child,
...@@ -113,13 +114,20 @@ class TextButton extends ButtonStyleButton { ...@@ -113,13 +114,20 @@ class TextButton extends ButtonStyleButton {
/// ///
/// The [foregroundColor] and [disabledForegroundColor] colors are used /// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and /// to create a [MaterialStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor]. /// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
/// ///
/// The [backgroundColor] and [disabledBackgroundColor] colors are /// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor]. /// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
/// ///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor] /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor]. /// parameters are used to construct [ButtonStyle.mouseCursor] and
/// [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor].
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [MaterialStateProperty] with the same opacities as the
/// default is created.
/// ///
/// All of the other parameters are either used directly or used to /// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all /// create a [MaterialStateProperty] with a single value for all
...@@ -151,6 +159,7 @@ class TextButton extends ButtonStyleButton { ...@@ -151,6 +159,7 @@ class TextButton extends ButtonStyleButton {
Color? surfaceTintColor, Color? surfaceTintColor,
Color? iconColor, Color? iconColor,
Color? disabledIconColor, Color? disabledIconColor,
Color? overlayColor,
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
...@@ -167,32 +176,33 @@ class TextButton extends ButtonStyleButton { ...@@ -167,32 +176,33 @@ class TextButton extends ButtonStyleButton {
bool? enableFeedback, bool? enableFeedback,
AlignmentGeometry? alignment, AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory, InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) { }) {
final Color? foreground = foregroundColor; final MaterialStateProperty<Color?>? foregroundColorProp = switch ((foregroundColor, disabledForegroundColor)) {
final Color? disabledForeground = disabledForegroundColor; (null, null) => null,
final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null) (_, _) => _TextButtonDefaultColor(foregroundColor, disabledForegroundColor),
? null };
: _TextButtonDefaultColor(foreground, disabledForeground); final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null) (null, null) => null,
? null (_, _) => _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor),
: disabledBackgroundColor == null };
? ButtonStyleButton.allOrNull<Color?>(backgroundColor) final MaterialStateProperty<Color?>? iconColorProp = switch ((iconColor, disabledIconColor)) {
: _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor); (null, null) => null,
final MaterialStateProperty<Color?>? overlayColor = (foreground == null) (_, _) => _TextButtonDefaultColor(iconColor, disabledIconColor),
? null };
: _TextButtonDefaultOverlay(foreground); final MaterialStateProperty<Color?>? overlayColorProp = switch ((foregroundColor, overlayColor)) {
final MaterialStateProperty<Color?>? iconColorProp = (iconColor == null && disabledIconColor == null) (null, null) => null,
? null (_, final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
: disabledIconColor == null (_, _) => _TextButtonDefaultOverlay((overlayColor ?? foregroundColor)!),
? ButtonStyleButton.allOrNull<Color?>(iconColor) };
: _TextButtonDefaultIconColor(iconColor, disabledIconColor);
final MaterialStateProperty<MouseCursor?> mouseCursor = _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor); final MaterialStateProperty<MouseCursor?> mouseCursor = _TextButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
return ButtonStyle( return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle), textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: foregroundColorProp, foregroundColor: foregroundColorProp,
backgroundColor: backgroundColorProp, backgroundColor: backgroundColorProp,
overlayColor: overlayColor, overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor), shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor), surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp, iconColor: iconColorProp,
...@@ -210,6 +220,8 @@ class TextButton extends ButtonStyleButton { ...@@ -210,6 +220,8 @@ class TextButton extends ButtonStyleButton {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
splashFactory: splashFactory, splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
} }
...@@ -250,7 +262,7 @@ class TextButton extends ButtonStyleButton { ...@@ -250,7 +262,7 @@ class TextButton extends ButtonStyleButton {
/// * disabled - Theme.colorScheme.onSurface(0.38) /// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary /// * others - Theme.colorScheme.primary
/// * `overlayColor` /// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.04) /// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12) /// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor /// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0 /// * `elevation` - 0
...@@ -424,27 +436,6 @@ class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> { ...@@ -424,27 +436,6 @@ class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> {
} }
} }
@immutable
class _TextButtonDefaultIconColor extends MaterialStateProperty<Color?> {
_TextButtonDefaultIconColor(this.iconColor, this.disabledIconColor);
final Color? iconColor;
final Color? disabledIconColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledIconColor;
}
return iconColor;
}
@override
String toString() {
return '{disabled: $disabledIconColor, color: $iconColor}';
}
}
@immutable @immutable
class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable { class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
_TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor); _TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
...@@ -471,13 +462,12 @@ class _TextButtonWithIcon extends TextButton { ...@@ -471,13 +462,12 @@ class _TextButtonWithIcon extends TextButton {
super.style, super.style,
super.focusNode, super.focusNode,
bool? autofocus, bool? autofocus,
Clip? clipBehavior, super.clipBehavior,
super.statesController, super.statesController,
required Widget icon, required Widget icon,
required Widget label, required Widget label,
}) : super( }) : super(
autofocus: autofocus ?? false, autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _TextButtonWithIconChild(icon: icon, label: label, buttonStyle: style), child: _TextButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
); );
......
...@@ -42,6 +42,8 @@ void main() { ...@@ -42,6 +42,8 @@ void main() {
expect(style.tapTargetSize, null); expect(style.tapTargetSize, null);
expect(style.animationDuration, null); expect(style.animationDuration, null);
expect(style.enableFeedback, null); expect(style.enableFeedback, null);
expect(style.backgroundBuilder, null);
expect(style.foregroundBuilder, null);
}); });
testWidgets('Default ButtonStyle debugFillProperties', (WidgetTester tester) async { testWidgets('Default ButtonStyle debugFillProperties', (WidgetTester tester) async {
...@@ -107,6 +109,9 @@ void main() { ...@@ -107,6 +109,9 @@ void main() {
}); });
testWidgets('ButtonStyle copyWith, merge', (WidgetTester tester) async { testWidgets('ButtonStyle copyWith, merge', (WidgetTester tester) async {
Widget backgroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) => child!;
Widget foregroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) => child!;
const MaterialStateProperty<TextStyle> textStyle = MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 10)); const MaterialStateProperty<TextStyle> textStyle = MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 10));
const MaterialStateProperty<Color> backgroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff1)); const MaterialStateProperty<Color> backgroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff1));
const MaterialStateProperty<Color> foregroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff2)); const MaterialStateProperty<Color> foregroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff2));
...@@ -128,7 +133,7 @@ void main() { ...@@ -128,7 +133,7 @@ void main() {
const Duration animationDuration = Duration(seconds: 1); const Duration animationDuration = Duration(seconds: 1);
const bool enableFeedback = true; const bool enableFeedback = true;
const ButtonStyle style = ButtonStyle( final ButtonStyle style = ButtonStyle(
textStyle: textStyle, textStyle: textStyle,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
...@@ -149,6 +154,8 @@ void main() { ...@@ -149,6 +154,8 @@ void main() {
tapTargetSize: tapTargetSize, tapTargetSize: tapTargetSize,
animationDuration: animationDuration, animationDuration: animationDuration,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
expect( expect(
...@@ -174,6 +181,8 @@ void main() { ...@@ -174,6 +181,8 @@ void main() {
tapTargetSize: tapTargetSize, tapTargetSize: tapTargetSize,
animationDuration: animationDuration, animationDuration: animationDuration,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
backgroundBuilder: backgroundBuilder,
foregroundBuilder:foregroundBuilder,
), ),
); );
......
...@@ -1942,6 +1942,203 @@ void main() { ...@@ -1942,6 +1942,203 @@ void main() {
expect(controller.value, <MaterialState>{MaterialState.disabled}); expect(controller.value, <MaterialState>{MaterialState.disabled});
expect(count, 1); expect(count, 1);
}); });
testWidgets('ElevatedButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('ElevatedButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('ElevatedButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(ElevatedButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('ElevatedButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(ElevatedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// 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(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -2014,6 +2014,202 @@ void main() { ...@@ -2014,6 +2014,202 @@ void main() {
expect(count, 1); expect(count, 1);
}); });
testWidgets('FilledButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('FilledButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('FilledButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FilledButton(
style: FilledButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(FilledButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('FilledButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// 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(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// 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(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -2092,6 +2092,203 @@ void main() { ...@@ -2092,6 +2092,203 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('OutlinedButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('OutlinedButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('OutlinedButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(OutlinedButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('OutlinedButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlinedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// 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(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle _iconStyle(WidgetTester tester, IconData icon) { TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -1925,6 +1925,203 @@ void main() { ...@@ -1925,6 +1925,203 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('TextButton backgroundBuilder and foregroundBuilder', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: backgroundColor,
),
child: child,
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return DecoratedBox(
decoration: const BoxDecoration(
color: foregroundColor,
),
child: child,
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
BoxDecoration boxDecorationOf(Finder finder) {
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
}
final Finder decorations = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
Text textChildOf(Finder finder) {
return tester.widget<Text>(
find.descendant(
of: finder,
matching: find.byType(Text),
),
);
}
expect(textChildOf(decorations.at(0)).data, 'button');
expect(textChildOf(decorations.at(1)).data, 'button');
});
testWidgets('TextButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async {
const Color backgroundColor = Color(0xFF000011);
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
);
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder background = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(background, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('TextButton foregroundBuilder drops button child', (WidgetTester tester) async {
const Color foregroundColor = Color(0xFF000022);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
style: TextButton.styleFrom(
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
return const DecoratedBox(
decoration: BoxDecoration(
color: foregroundColor,
),
);
},
),
onPressed: () { },
child: const Text('button'),
),
),
);
final Finder foreground = find.descendant(
of: find.byType(TextButton),
matching: find.byType(DecoratedBox),
);
expect(foreground, findsOneWidget);
expect(find.text('button'), findsNothing);
});
testWidgets('TextButton foreground and background builders are applied to the correct states', (WidgetTester tester) async {
Set<MaterialState> foregroundStates = <MaterialState>{};
Set<MaterialState> backgroundStates = <MaterialState>{};
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
style: ButtonStyle(
backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
backgroundStates = states;
return child!;
},
foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
foregroundStates = states;
return child!;
},
),
onPressed: () {},
focusNode: focusNode,
child: const Text('button'),
),
),
),
),
);
// Default.
expect(backgroundStates.isEmpty, isTrue);
expect(foregroundStates.isEmpty, isTrue);
const Set<MaterialState> focusedStates = <MaterialState>{MaterialState.focused};
const Set<MaterialState> focusedHoveredStates = <MaterialState>{MaterialState.focused, MaterialState.hovered};
const Set<MaterialState> focusedHoveredPressedStates = <MaterialState>{MaterialState.focused, MaterialState.hovered, MaterialState.pressed};
bool sameStates(Set<MaterialState> expectedValue, Set<MaterialState> actualValue) {
return expectedValue.difference(actualValue).isEmpty && actualValue.difference(expectedValue).isEmpty;
}
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(sameStates(focusedStates, backgroundStates), isTrue);
expect(sameStates(focusedStates, foregroundStates), isTrue);
// Hovered.
final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
// 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(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
focusNode.dispose();
});
} }
TextStyle? _iconStyle(WidgetTester tester, IconData icon) { TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
......
...@@ -104,6 +104,15 @@ void main() { ...@@ -104,6 +104,15 @@ void main() {
const bool enableFeedback = false; const bool enableFeedback = false;
const AlignmentGeometry alignment = Alignment.centerLeft; const AlignmentGeometry alignment = Alignment.centerLeft;
final Key backgroundKey = UniqueKey();
final Key foregroundKey = UniqueKey();
Widget backgroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) {
return KeyedSubtree(key: backgroundKey, child: child!);
}
Widget foregroundBuilder(BuildContext context, Set<MaterialState> states, Widget? child) {
return KeyedSubtree(key: foregroundKey, child: child!);
}
final ButtonStyle style = TextButton.styleFrom( final ButtonStyle style = TextButton.styleFrom(
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
disabledForegroundColor: disabledColor, disabledForegroundColor: disabledColor,
...@@ -122,6 +131,8 @@ void main() { ...@@ -122,6 +131,8 @@ void main() {
animationDuration: animationDuration, animationDuration: animationDuration,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
); );
Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) { Widget buildFrame({ ButtonStyle? buttonStyle, ButtonStyle? themeStyle, ButtonStyle? overallStyle }) {
...@@ -185,6 +196,8 @@ void main() { ...@@ -185,6 +196,8 @@ void main() {
expect(tester.getSize(find.byType(TextButton)), const Size(200, 200)); expect(tester.getSize(find.byType(TextButton)), const Size(200, 200));
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align))); final Align align = tester.firstWidget<Align>(find.ancestor(of: find.text('button'), matching: find.byType(Align)));
expect(align.alignment, alignment); expect(align.alignment, alignment);
expect(find.descendant(of: findMaterial, matching: find.byKey(backgroundKey)), findsOneWidget);
expect(find.descendant(of: findInkWell, matching: find.byKey(foregroundKey)), findsOneWidget);
} }
testWidgets('Button style overrides defaults', (WidgetTester tester) async { testWidgets('Button style overrides defaults', (WidgetTester tester) async {
......
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