• Hans Muller's avatar
    Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#141818) · ff6c8f5d
    Hans Muller authored
    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'),
    )
    ```
    ff6c8f5d
Name
Last commit
Last update
..
about Loading commit data...
action_buttons Loading commit data...
action_chip Loading commit data...
animated_icon Loading commit data...
app Loading commit data...
app_bar Loading commit data...
autocomplete Loading commit data...
banner Loading commit data...
bottom_app_bar Loading commit data...
bottom_navigation_bar Loading commit data...
bottom_sheet Loading commit data...
button_style Loading commit data...
card Loading commit data...
checkbox Loading commit data...
checkbox_list_tile Loading commit data...
chip Loading commit data...
choice_chip Loading commit data...
color_scheme Loading commit data...
context_menu Loading commit data...
data_table Loading commit data...
date_picker Loading commit data...
dialog Loading commit data...
divider Loading commit data...
drawer Loading commit data...
dropdown Loading commit data...
dropdown_menu Loading commit data...
elevated_button Loading commit data...
expansion_panel Loading commit data...
expansion_tile Loading commit data...
filled_button Loading commit data...
filter_chip Loading commit data...
flexible_space_bar Loading commit data...
floating_action_button Loading commit data...
floating_action_button_location Loading commit data...
icon_button Loading commit data...
ink Loading commit data...
ink_well Loading commit data...
input_chip Loading commit data...
input_decorator Loading commit data...
list_tile Loading commit data...
material_state Loading commit data...
menu_anchor Loading commit data...
navigation_bar Loading commit data...
navigation_drawer Loading commit data...
navigation_rail Loading commit data...
outlined_button Loading commit data...
page_transitions_theme Loading commit data...
paginated_data_table Loading commit data...
platform_menu_bar Loading commit data...
popup_menu Loading commit data...
progress_indicator Loading commit data...
radio Loading commit data...
radio_list_tile Loading commit data...
range_slider Loading commit data...
refresh_indicator Loading commit data...
reorderable_list Loading commit data...
scaffold Loading commit data...
scrollbar Loading commit data...
search_anchor Loading commit data...
segmented_button Loading commit data...
selectable_region Loading commit data...
selection_area Loading commit data...
selection_container Loading commit data...
slider Loading commit data...
snack_bar Loading commit data...
stepper Loading commit data...
switch Loading commit data...
switch_list_tile Loading commit data...
tab_controller Loading commit data...
tabs Loading commit data...
text_button Loading commit data...
text_field Loading commit data...
text_form_field Loading commit data...
theme Loading commit data...
theme_data Loading commit data...
time_picker Loading commit data...
toggle_buttons Loading commit data...
tooltip Loading commit data...