Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder #141818

Merged
merged 12 commits into from Feb 1, 2024

Conversation

HansMuller
Copy link
Contributor

@HansMuller HansMuller commented Jan 18, 2024

Fixes #139456, #130335, #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:

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

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

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

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

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

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

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

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

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

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

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'),
)

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jan 18, 2024
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples in the PR description look very cool!

Mostly just doc nits asking for more context.

/// 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].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to include some of the examples from the PR's description here and on the other builder to show how powerful these are and what one can do with them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also include a note about making sure that highlights come through by either using a non-opaque background color or the Ink widget.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely planning to write more tests and to add API doc examples, yes!

packages/flutter/lib/src/material/elevated_button.dart Outdated Show resolved Hide resolved
@HansMuller HansMuller force-pushed the button_style_layers branch 2 times, most recently from 7b94242 to 41dbd62 Compare January 25, 2024 23:49
@github-actions github-actions bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Jan 29, 2024
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some doc nits.

final Color color1;
final Color color2;
final Color color3;
if (colorScheme.brightness == Brightness.light) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

// This theme defines default property overrides for all of the buttons
// that follow.
TextButtonTheme
(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: formatting here is odd, this ( should probably be on the previous line?

),
verticalSpacer,

// Override button's shape its border.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something seems to be missing in this sentence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks for catching that.

// Override the foregroundBuilder to specify images for the button's pressed
// hovered and inactive states.
//
// This is an example of completely change the default appearance of a button
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change -> changing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fixed.

// overlayColor: Colors.transparent. AnimatedContainer takes care of the
// fade in and out segues between images.
//
// The foregroundBuilder its child parameter. Unfortunately TextButton's child
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what "The foregroundBuilder its child parameter" means.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have been // This foregroundBuilder function ignores its child parameter.. Fixed. Thanks for catching that.

/// instead of the button's child.
///
/// The returned widget is clipped by the button's
/// [ButtonStyle.shape] inset by the button's [ButtonStyle.padding]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a comma before inset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Thanks and fixed.

///
/// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be

Suggested change
/// parameters are used to construct [ButtonStyle].mouseCursor and
/// parameters are used to construct [ButtonStyle.mouseCursor] and

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fixed.

///
/// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
/// parameters are used to construct [ButtonStyle].mouseCursor and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// parameters are used to construct [ButtonStyle].mouseCursor and
/// parameters are used to construct [ButtonStyle.mouseCursor] and

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was consistently wrong, thanks for catching that.

Comment on lines 69 to 81
final Color color1;
final Color color2;
final Color color3;
switch (colorScheme.brightness) {
case Brightness.light:
color1 = Colors.blue.withOpacity(0.5);
color2 = Colors.orange.withOpacity(0.5);
color3 = Colors.yellow.withOpacity(0.5);
case Brightness.dark:
color1 = Colors.purple.withOpacity(0.5);
color2 = Colors.cyan.withOpacity(0.5);
color3 = Colors.yellow.withOpacity(0.5);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final Color color1;
final Color color2;
final Color color3;
switch (colorScheme.brightness) {
case Brightness.light:
color1 = Colors.blue.withOpacity(0.5);
color2 = Colors.orange.withOpacity(0.5);
color3 = Colors.yellow.withOpacity(0.5);
case Brightness.dark:
color1 = Colors.purple.withOpacity(0.5);
color2 = Colors.cyan.withOpacity(0.5);
color3 = Colors.yellow.withOpacity(0.5);
}
final (Color color1, Color color2, Color color3) = switch (colorScheme.brightness) {
Brightness.light => (
Colors.blue.withOpacity(0.5),
Colors.orange.withOpacity(0.5),
Colors.yellow.withOpacity(0.5),
),
Brightness.dark => (
Colors.purple.withOpacity(0.5),
Colors.cyan.withOpacity(0.5),
Colors.yellow.withOpacity(0.5),
),
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so much better. It's hard (It was hard for me) to find an example of this syntax in action. Hopefully we're helping spread the word.

@HansMuller HansMuller force-pushed the button_style_layers branch 2 times, most recently from 5898592 to 4881a5b Compare January 31, 2024 17:55
Copy link
Member

@TahaTesser TahaTesser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

super.dispose();
scrollController.dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
super.dispose();
scrollController.dispose();
scrollController.dispose();
super.dispose();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 187 to 189
];


final List<Widget> columnTwoButtons = <Widget>[
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
];
final List<Widget> columnTwoButtons = <Widget>[
];
final List<Widget> columnTwoButtons = <Widget>[

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@HansMuller HansMuller added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 31, 2024
@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 31, 2024
Copy link
Contributor

auto-submit bot commented Jan 31, 2024

auto label is removed for flutter/flutter/141818, due to - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@HansMuller HansMuller added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 31, 2024
@auto-submit auto-submit bot merged commit ff6c8f5 into flutter:master Feb 1, 2024
138 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 1, 2024
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Feb 1, 2024
…6028)

Manual roll Flutter from c65ab4d513da to e02e2079bea7 (38 revisions)

Manual roll requested by stuartmorgan@google.com

flutter/flutter@c65ab4d...e02e207

2024-02-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from f4fbabf1eb9f to 68943afd62d1 (9 revisions) (flutter/flutter#142690)
2024-02-01 36861262+QuncCccccc@users.noreply.github.com Introduce tone-based surfaces and accent color add-ons - Part 1 (flutter/flutter#142654)
2024-02-01 andrewrkolos@gmail.com improve error message when `--base-href` argument does not start with `/` (flutter/flutter#142667)
2024-02-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from c4247c5e31ba to f4fbabf1eb9f (1 revision) (flutter/flutter#142675)
2024-02-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from c83617eee093 to c4247c5e31ba (3 revisions) (flutter/flutter#142662)
2024-02-01 jonahwilliams@google.com [Impeller] opt vulkan tests into GPU tracing. (flutter/flutter#142649)
2024-02-01 gspencergoog@users.noreply.github.com Convert button `.icon` and `.tonalIcon` constructors to take nullable icons. (flutter/flutter#142644)
2024-02-01 davidmartos96@gmail.com Fix token usages on Regular Chip and Action Chip (flutter/flutter#141701)
2024-02-01 hans.muller@gmail.com Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (flutter/flutter#141818)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from 5b89189b8b5f to c83617eee093 (2 revisions) (flutter/flutter#142656)
2024-01-31 christopherfujino@gmail.com [flutter_tools] add debugging to ios/core_devices.dart (flutter/flutter#142187)
2024-01-31 gspencergoog@users.noreply.github.com Fix showDialog docs (flutter/flutter#142458)
2024-01-31 49699333+dependabot[bot]@users.noreply.github.com Bump peter-evans/create-pull-request from 5.0.2 to 6.0.0 (flutter/flutter#142650)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from 20e53614c16c to 5b89189b8b5f (2 revisions) (flutter/flutter#142640)
2024-01-31 dnfield@google.com Refactor ShaderTarget to not explicitly mention impeller or Skia (flutter/flutter#141460)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from 9ccd81d7595b to 20e53614c16c (3 revisions) (flutter/flutter#142628)
2024-01-31 louisehsu@google.com Show Mac Designed For iPad in 'flutter devices' (flutter/flutter#141718)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_arm64_ios basic_material_app_ios__compile to be unflaky (flutter/flutter#142594)
2024-01-31 goderbauer@google.com Fix ParentDataWidget crash for  multi view scenarios (flutter/flutter#142486)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from e0d8f472a1b6 to 9ccd81d7595b (1 revision) (flutter/flutter#142625)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_arm64 tool_tests_commands to be unflaky (flutter/flutter#142593)
2024-01-31 jmccandless@google.com "System back gesture" explanation (flutter/flutter#142254)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_x64 tool_tests_commands to be unflaky (flutter/flutter#142592)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_x64_ios integration_test_test_ios to be unflaky (flutter/flutter#142595)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_x64 native_ui_tests_macos to be unflaky (flutter/flutter#142598)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_x64_ios hot_mode_dev_cycle_ios__benchmark to be unflaky (flutter/flutter#142597)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_arm64 native_ui_tests_macos to be unflaky (flutter/flutter#142599)
2024-01-31 fluttergithubbot@gmail.com Marks Windows_android hot_mode_dev_cycle_win__benchmark to be flaky (flutter/flutter#142609)
2024-01-31 fluttergithubbot@gmail.com Marks Mac_arm64_ios integration_test_test_ios to be unflaky (flutter/flutter#142596)
2024-01-31 polinach@google.com Mark test that leaks image. (flutter/flutter#142539)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from b9bc256156b8 to e0d8f472a1b6 (1 revision) (flutter/flutter#142623)
2024-01-31 31859944+LongCatIsLooong@users.noreply.github.com Fix unresponsive mouse tooltip (flutter/flutter#142282)
2024-01-31 fluttergithubbot@gmail.com Marks Linux_android_emu android_defines_test to be unflaky (flutter/flutter#142591)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from 447dd212447e to b9bc256156b8 (6 revisions) (flutter/flutter#142617)
2024-01-31 engine-flutter-autoroll@skia.org Roll Packages from 25abb5d to 5b48c44 (4 revisions) (flutter/flutter#142616)
2024-01-31 64037520+SelaseKay@users.noreply.github.com Fix null operator error when tapping on 'MenuItemButton' (flutter/flutter#142230)
2024-01-31 engine-flutter-autoroll@skia.org Roll Flutter Engine from 8e7df85f7d11 to 447dd212447e (2 revisions) (flutter/flutter#142587)
2024-01-31 katelovett@google.com Split out AppBar/SliverAppBar material tests (flutter/flutter#142560)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.
...
@XilaiZhang XilaiZhang added the revert Autorevert PR (with "Reason for revert:" comment) label Feb 1, 2024
auto-submit bot pushed a commit that referenced this pull request Feb 1, 2024
@auto-submit auto-submit bot removed the revert Autorevert PR (with "Reason for revert:" comment) label Feb 1, 2024
auto-submit bot added a commit that referenced this pull request Feb 1, 2024
…ndBuilder" (#142748)

Reverts #141818
Initiated by: XilaiZhang
This change reverts the following previous change:
Original Description:
Fixes #139456, #130335, #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'),
)
```
@HansMuller HansMuller deleted the button_style_layers branch February 1, 2024 23:56
auto-submit bot pushed a commit that referenced this pull request Feb 2, 2024
…dBuilder (#142762)

Reland #141818 with a fix for a special case: If only `background` is specified for `TextButton.styleFrom` or `OutlinedButton.styleFrom` it applies the button's disabled state, i.e. as if the same value had been specified for disabledBackgroundColor.

The change relative to #141818 is the indicated line below:
```dart
final MaterialStateProperty<Color?>? backgroundColorProp = switch ((backgroundColor, disabledBackgroundColor)) {
  (null, null) => null,
  (_, null) => MaterialStatePropertyAll<Color?>(backgroundColor), // ADDED THIS LINE
  (_, _) => _TextButtonDefaultColor(backgroundColor, disabledBackgroundColor),
};
  ```

This backwards incompatibility cropped up in an internal test, see internal Google issue b/323399158.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow arbitrary foreground and background elements for ButtonStyle
4 participants