Add IconAlignment to ButtonStyle and styleFrom methods (#158503)

Fixes [Proposal to add iconAlignment to
ButtonStyle](https://github.com/flutter/flutter/issues/153350)

### Description

This PR refactors buttons `IconAlignment`, adds to `ButtonStyle` and
`styleFrom` methods. Which makes it possible to customize iconAlignment
same way as icon size and color in the `ButtonStyle`.

### Code sample 

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

enum StyleSegment {
  none,
  widgetButtonStyle,
  widgetStyleFrom,
  themeButtonStyle,
  themeStyleFrom
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  StyleSegment _selectedSegment = StyleSegment.none;

  ThemeData? getThemeStyle() => switch (_selectedSegment) {
        StyleSegment.themeButtonStyle => ThemeData(
            textButtonTheme: const TextButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
            elevatedButtonTheme: const ElevatedButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
            outlinedButtonTheme: const OutlinedButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
            filledButtonTheme: const FilledButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
          ),
        StyleSegment.themeStyleFrom => ThemeData(
            textButtonTheme: TextButtonThemeData(
              style: TextButton.styleFrom(
                iconAlignment: IconAlignment.end,
              ),
            ),
            elevatedButtonTheme: const ElevatedButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
            outlinedButtonTheme: const OutlinedButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
            filledButtonTheme: const FilledButtonThemeData(
              style: ButtonStyle(
                iconAlignment: IconAlignment.end,
              ),
            ),
          ),
        _ => null
      };

  ButtonStyle? getTextButtonStyle() => switch (_selectedSegment) {
        StyleSegment.widgetStyleFrom => TextButton.styleFrom(
            iconAlignment: IconAlignment.end,
          ),
        StyleSegment.widgetButtonStyle => const ButtonStyle(
            iconAlignment: IconAlignment.end,
          ),
        _ => null
      };

  ButtonStyle? getElevatedButtonStyle() => switch (_selectedSegment) {
        StyleSegment.widgetStyleFrom => ElevatedButton.styleFrom(
            iconAlignment: IconAlignment.end,
          ),
        StyleSegment.widgetButtonStyle => const ButtonStyle(
            iconAlignment: IconAlignment.end,
          ),
        _ => null
      };

  ButtonStyle? getOutlinedButtonStyle() => switch (_selectedSegment) {
        StyleSegment.widgetStyleFrom => OutlinedButton.styleFrom(
            iconAlignment: IconAlignment.end,
          ),
        StyleSegment.widgetButtonStyle => const ButtonStyle(
            iconAlignment: IconAlignment.end,
          ),
        _ => null
      };

  ButtonStyle? getFilledButtonStyle() => switch (_selectedSegment) {
        StyleSegment.widgetStyleFrom => FilledButton.styleFrom(
            iconAlignment: IconAlignment.end,
          ),
        StyleSegment.widgetButtonStyle => const ButtonStyle(
            iconAlignment: IconAlignment.end,
          ),
        _ => null
      };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: getThemeStyle(),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ButtonStyle Icon Alignment'),
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              spacing: 20,
              children: [
                Wrap(
                  spacing: 16,
                  runSpacing: 16,
                  children: [
                    TextButton.icon(
                      style: getTextButtonStyle(),
                      onPressed: () {},
                      icon: const Icon(Icons.add),
                      label: const Text('Text Button'),
                    ),
                    ElevatedButton.icon(
                      style: getElevatedButtonStyle(),
                      onPressed: () {},
                      icon: const Icon(Icons.add),
                      label: const Text('Elevated Button'),
                    ),
                    OutlinedButton.icon(
                      style: getOutlinedButtonStyle(),
                      onPressed: () {},
                      icon: const Icon(Icons.add),
                      label: const Text('Outlined Button'),
                    ),
                    FilledButton.icon(
                      style: getFilledButtonStyle(),
                      onPressed: () {},
                      icon: const Icon(Icons.add),
                      label: const Text('Filled Button'),
                    ),
                    FilledButton.tonalIcon(
                      style: getFilledButtonStyle(),
                      onPressed: () {},
                      icon: const Icon(Icons.add),
                      label: const Text('Filled Button Tonal Icon'),
                    ),
                  ],
                ),
                StyleSelection(
                  selectedSegment: _selectedSegment,
                  onSegmentSelected: (StyleSegment segment) {
                    setState(() {
                      _selectedSegment = segment;
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class StyleSelection extends StatelessWidget {
  const StyleSelection(
      {super.key,
      this.selectedSegment = StyleSegment.none,
      required this.onSegmentSelected});

  final ValueChanged<StyleSegment> onSegmentSelected;
  final StyleSegment selectedSegment;

  @override
  Widget build(BuildContext context) {
    return SegmentedButton<StyleSegment>(
      segments: const <ButtonSegment<StyleSegment>>[
        ButtonSegment<StyleSegment>(
          value: StyleSegment.none,
          label: Text('None'),
        ),
        ButtonSegment<StyleSegment>(
          value: StyleSegment.widgetButtonStyle,
          label: Text('Widget Button Style'),
        ),
        ButtonSegment<StyleSegment>(
          value: StyleSegment.widgetStyleFrom,
          label: Text('Widget Style From'),
        ),
        ButtonSegment<StyleSegment>(
          value: StyleSegment.themeButtonStyle,
          label: Text('Theme Button Style'),
        ),
        ButtonSegment<StyleSegment>(
          value: StyleSegment.themeStyleFrom,
          label: Text('Theme Style From'),
        ),
      ],
      selected: <StyleSegment>{selectedSegment},
      onSelectionChanged: (Set<StyleSegment> newSelection) {
        onSegmentSelected(newSelection.first);
      },
    );
  }
}
```

</details>

### Preview

<img width="1175" alt="Screenshot 2024-11-12 at 12 10 43"
src="https://github.com/user-attachments/assets/a28207c5-0ef7-41fa-a45c-e9401df897a0">


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Taha Tesser 2024-12-03 23:19:12 +02:00 committed by GitHub
parent 8106f2ad1c
commit 9e2d9deb28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 443 additions and 47 deletions

View File

@ -23,6 +23,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style_button.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'theme_data.dart';
@ -174,6 +175,7 @@ class ButtonStyle with Diagnosticable {
this.maximumSize,
this.iconColor,
this.iconSize,
this.iconAlignment,
this.side,
this.shape,
this.mouseCursor,
@ -279,6 +281,22 @@ class ButtonStyle with Diagnosticable {
/// The icon's size inside of the button.
final MaterialStateProperty<double?>? iconSize;
/// The alignment of the button's icon.
///
/// This property is supported for the following button types:
///
/// * [ElevatedButton.icon].
/// * [FilledButton.icon].
/// * [FilledButton.tonalIcon].
/// * [OutlinedButton.icon].
/// * [TextButton.icon].
///
/// See also:
///
/// * [IconAlignment], for more information about the different icon
/// alignments.
final IconAlignment? iconAlignment;
/// The color and weight of the button's outline.
///
/// This value is combined with [shape] to create a shape decorated
@ -407,6 +425,7 @@ class ButtonStyle with Diagnosticable {
MaterialStateProperty<Size?>? maximumSize,
MaterialStateProperty<Color?>? iconColor,
MaterialStateProperty<double?>? iconSize,
IconAlignment? iconAlignment,
MaterialStateProperty<BorderSide?>? side,
MaterialStateProperty<OutlinedBorder?>? shape,
MaterialStateProperty<MouseCursor?>? mouseCursor,
@ -433,6 +452,7 @@ class ButtonStyle with Diagnosticable {
maximumSize: maximumSize ?? this.maximumSize,
iconColor: iconColor ?? this.iconColor,
iconSize: iconSize ?? this.iconSize,
iconAlignment: iconAlignment ?? this.iconAlignment,
side: side ?? this.side,
shape: shape ?? this.shape,
mouseCursor: mouseCursor ?? this.mouseCursor,
@ -470,6 +490,7 @@ class ButtonStyle with Diagnosticable {
maximumSize: maximumSize ?? style.maximumSize,
iconColor: iconColor ?? style.iconColor,
iconSize: iconSize ?? style.iconSize,
iconAlignment: iconAlignment ?? style.iconAlignment,
side: side ?? style.side,
shape: shape ?? style.shape,
mouseCursor: mouseCursor ?? style.mouseCursor,
@ -500,6 +521,7 @@ class ButtonStyle with Diagnosticable {
maximumSize,
iconColor,
iconSize,
iconAlignment,
side,
shape,
mouseCursor,
@ -537,6 +559,7 @@ class ButtonStyle with Diagnosticable {
&& other.maximumSize == maximumSize
&& other.iconColor == iconColor
&& other.iconSize == iconSize
&& other.iconAlignment == iconAlignment
&& other.side == side
&& other.shape == shape
&& other.mouseCursor == mouseCursor
@ -566,6 +589,7 @@ class ButtonStyle with Diagnosticable {
properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize', maximumSize, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('iconColor', iconColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('iconSize', iconSize, defaultValue: null));
properties.add(EnumProperty<IconAlignment>('iconAlignment', iconAlignment, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side', side, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
@ -597,6 +621,7 @@ class ButtonStyle with Diagnosticable {
maximumSize: MaterialStateProperty.lerp<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp),
iconColor: MaterialStateProperty.lerp<Color?>(a?.iconColor, b?.iconColor, t, Color.lerp),
iconSize: MaterialStateProperty.lerp<double?>(a?.iconSize, b?.iconSize, t, lerpDouble),
iconAlignment: t < 0.5 ? a?.iconAlignment : b?.iconAlignment,
side: _lerpSides(a?.side, b?.side, t),
shape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.shape, b?.shape, t, OutlinedBorder.lerp),
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,

View File

@ -86,7 +86,7 @@ abstract class ButtonStyleButton extends StatefulWidget {
required this.clipBehavior,
this.statesController,
this.isSemanticButton = true,
this.iconAlignment = IconAlignment.start,
this.iconAlignment,
this.tooltip,
required this.child,
});
@ -158,7 +158,7 @@ abstract class ButtonStyleButton extends StatefulWidget {
final bool? isSemanticButton;
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
final IconAlignment iconAlignment;
final IconAlignment? iconAlignment;
/// Text that describes the action that will occur when the button is pressed or
/// hovered over.

View File

@ -80,7 +80,6 @@ class ElevatedButton extends ButtonStyleButton {
super.clipBehavior,
super.statesController,
required super.child,
super.iconAlignment,
});
/// Create an elevated button from a pair of widgets that serve as the button's
@ -106,7 +105,7 @@ class ElevatedButton extends ButtonStyleButton {
MaterialStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment iconAlignment = IconAlignment.start,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return ElevatedButton(
@ -210,6 +209,7 @@ class ElevatedButton extends ButtonStyleButton {
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
@ -265,6 +265,7 @@ class ElevatedButton extends ButtonStyleButton {
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: elevationValue,
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -478,7 +479,7 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
super.statesController,
required Widget icon,
required Widget label,
super.iconAlignment,
IconAlignment? iconAlignment,
}) : super(
autofocus: autofocus ?? false,
child: _ElevatedButtonWithIconChild(
@ -525,16 +526,21 @@ class _ElevatedButtonWithIconChild extends StatelessWidget {
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment iconAlignment;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
final double defaultFontSize = buttonStyle?.textStyle?.resolve(const <MaterialState>{})?.fontSize ?? 14.0;
final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0;
final double gap = lerpDouble(8, 4, scale)!;
final ElevatedButtonThemeData elevatedButtonTheme = ElevatedButtonTheme.of(context);
final IconAlignment effectiveIconAlignment = iconAlignment
?? elevatedButtonTheme.style?.iconAlignment
?? buttonStyle?.iconAlignment
?? IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
children: iconAlignment == IconAlignment.start
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, SizedBox(width: gap), Flexible(child: label)]
: <Widget>[Flexible(child: label), SizedBox(width: gap), icon],
);

View File

@ -82,7 +82,6 @@ class FilledButton extends ButtonStyleButton {
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
super.iconAlignment,
}) : _variant = _FilledButtonVariant.filled;
/// Create a filled button from [icon] and [label].
@ -107,7 +106,7 @@ class FilledButton extends ButtonStyleButton {
MaterialStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment iconAlignment = IconAlignment.start,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return FilledButton(
@ -180,7 +179,7 @@ class FilledButton extends ButtonStyleButton {
MaterialStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment iconAlignment = IconAlignment.start,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return FilledButton.tonal(
@ -275,6 +274,7 @@ class FilledButton extends ButtonStyleButton {
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
@ -317,6 +317,7 @@ class FilledButton extends ButtonStyleButton {
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: ButtonStyleButton.allOrNull(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -501,7 +502,7 @@ class _FilledButtonWithIcon extends FilledButton {
super.statesController,
required Widget icon,
required Widget label,
super.iconAlignment,
IconAlignment? iconAlignment,
}) : super(
autofocus: autofocus ?? false,
child: _FilledButtonWithIconChild(
@ -525,7 +526,7 @@ class _FilledButtonWithIcon extends FilledButton {
super.statesController,
required Widget icon,
required Widget label,
required IconAlignment iconAlignment,
IconAlignment? iconAlignment,
}) : super.tonal(
autofocus: autofocus ?? false,
child: _FilledButtonWithIconChild(
@ -572,7 +573,7 @@ class _FilledButtonWithIconChild extends StatelessWidget {
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment iconAlignment;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
@ -581,9 +582,14 @@ class _FilledButtonWithIconChild extends StatelessWidget {
// Adjust the gap based on the text scale factor. Start at 8, and lerp
// to 4 based on how large the text is.
final double gap = lerpDouble(8, 4, scale)!;
final FilledButtonThemeData filledButtonTheme = FilledButtonTheme.of(context);
final IconAlignment effectiveIconAlignment = iconAlignment
?? filledButtonTheme.style?.iconAlignment
?? buttonStyle?.iconAlignment
?? IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
children: iconAlignment == IconAlignment.start
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, SizedBox(width: gap), Flexible(child: label)]
: <Widget>[Flexible(child: label), SizedBox(width: gap), icon],
);

View File

@ -84,7 +84,6 @@ class OutlinedButton extends ButtonStyleButton {
super.clipBehavior,
super.statesController,
required super.child,
super.iconAlignment,
});
/// Create a text button from a pair of widgets that serve as the button's
@ -110,7 +109,7 @@ class OutlinedButton extends ButtonStyleButton {
MaterialStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment iconAlignment = IconAlignment.start,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return OutlinedButton(
@ -197,6 +196,7 @@ class OutlinedButton extends ButtonStyleButton {
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
@ -243,6 +243,7 @@ class OutlinedButton extends ButtonStyleButton {
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -427,7 +428,7 @@ class _OutlinedButtonWithIcon extends OutlinedButton {
super.statesController,
required Widget icon,
required Widget label,
super.iconAlignment,
IconAlignment? iconAlignment,
}) : super(
autofocus: autofocus ?? false,
child: _OutlinedButtonWithIconChild(
@ -470,16 +471,21 @@ class _OutlinedButtonWithIconChild extends StatelessWidget {
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment iconAlignment;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
final double defaultFontSize = buttonStyle?.textStyle?.resolve(const <MaterialState>{})?.fontSize ?? 14.0;
final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0;
final double gap = lerpDouble(8, 4, scale)!;
final OutlinedButtonThemeData outlinedButtonTheme = OutlinedButtonTheme.of(context);
final IconAlignment effectiveIconAlignment = iconAlignment
?? outlinedButtonTheme.style?.iconAlignment
?? buttonStyle?.iconAlignment
?? IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
children: iconAlignment == IconAlignment.start
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, SizedBox(width: gap), Flexible(child: label)]
: <Widget>[Flexible(child: label), SizedBox(width: gap), icon],
);

View File

@ -93,7 +93,6 @@ class TextButton extends ButtonStyleButton {
super.statesController,
super.isSemanticButton,
required Widget super.child,
super.iconAlignment,
});
/// Create a text button from a pair of widgets that serve as the button's
@ -119,7 +118,7 @@ class TextButton extends ButtonStyleButton {
MaterialStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment iconAlignment = IconAlignment.start,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return TextButton(
@ -204,6 +203,7 @@ class TextButton extends ButtonStyleButton {
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
@ -254,6 +254,7 @@ class TextButton extends ButtonStyleButton {
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
@ -456,7 +457,7 @@ class _TextButtonWithIcon extends TextButton {
super.statesController,
required Widget icon,
required Widget label,
super.iconAlignment,
IconAlignment? iconAlignment,
}) : super(
autofocus: autofocus ?? false,
child: _TextButtonWithIconChild(
@ -496,16 +497,21 @@ class _TextButtonWithIconChild extends StatelessWidget {
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment iconAlignment;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
final double defaultFontSize = buttonStyle?.textStyle?.resolve(const <MaterialState>{})?.fontSize ?? 14.0;
final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0;
final double gap = lerpDouble(8, 4, scale)!;
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
final IconAlignment effectiveIconAlignment = iconAlignment
?? textButtonTheme.style?.iconAlignment
?? buttonStyle?.iconAlignment
?? IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
children: iconAlignment == IconAlignment.start
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, SizedBox(width: gap), Flexible(child: label)]
: <Widget>[Flexible(child: label), SizedBox(width: gap), icon],
);

View File

@ -2295,7 +2295,7 @@ void main() {
focusNode.dispose();
});
testWidgets('Default iconAlignment', (WidgetTester tester) async {
testWidgets('Default ElevatedButton icon alignment', (WidgetTester tester) async {
Widget buildWidget({ required TextDirection textDirection }) {
return MaterialApp(
home: Directionality(
@ -2330,7 +2330,7 @@ void main() {
expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge.
});
testWidgets('iconAlignment can be customized', (WidgetTester tester) async {
testWidgets('ElevatedButton icon alignment can be customized', (WidgetTester tester) async {
Widget buildWidget({
required TextDirection textDirection,
required IconAlignment iconAlignment,
@ -2407,6 +2407,35 @@ void main() {
expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge.
});
testWidgets('ElevatedButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
home: Center(
child: ElevatedButton.icon(
style: ButtonStyle(iconAlignment: iconAlignment),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
// Regression test for https://github.com/flutter/flutter/issues/154798.
testWidgets('ElevatedButton.styleFrom can customize the button icon', (WidgetTester tester) async {
const Color iconColor = Color(0xFFF000FF);
@ -2415,15 +2444,18 @@ void main() {
Widget buildButton({ bool enabled = true }) {
return MaterialApp(
home: Material(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
disabledIconColor: disabledIconColor,
child: Center(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
iconAlignment: IconAlignment.end,
disabledIconColor: disabledIconColor,
),
onPressed: enabled ? () {} : null,
icon: const Icon(Icons.add),
label: const Text('Button'),
),
onPressed: enabled ? () {} : null,
icon: const Icon(Icons.add),
label: const Text('Button'),
),
),
);
@ -2437,5 +2469,9 @@ void main() {
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -228,7 +228,7 @@ void main() {
});
});
testWidgets('Material 3: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material3 - ElevatedButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -299,7 +299,7 @@ void main() {
expect(material.shadowColor, shadowColor);
});
testWidgets('Material 2: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material2 - ElevatedButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -368,4 +368,40 @@ void main() {
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
testWidgets('ElevatedButton.icon respects ElevatedButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
theme: ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(iconAlignment: iconAlignment),
),
),
home: Scaffold(
body: Center(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
await tester.pumpAndSettle();
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -2408,7 +2408,7 @@ void main() {
focusNode.dispose();
});
testWidgets('Default iconAlignment', (WidgetTester tester) async {
testWidgets('Default FilledButton icon alignment', (WidgetTester tester) async {
Widget buildWidget({ required TextDirection textDirection }) {
return MaterialApp(
home: Directionality(
@ -2443,7 +2443,7 @@ void main() {
expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge.
});
testWidgets('iconAlignment can be customized', (WidgetTester tester) async {
testWidgets('FilledButton icon alignment can be customized', (WidgetTester tester) async {
Widget buildWidget({
required TextDirection textDirection,
required IconAlignment iconAlignment,
@ -2520,6 +2520,64 @@ void main() {
expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge.
});
testWidgets('FilledButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
home: Center(
child: FilledButton.icon(
style: ButtonStyle(iconAlignment: iconAlignment),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
testWidgets('FilledButton tonal button icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
home: Center(
child: FilledButton.tonalIcon(
style: ButtonStyle(iconAlignment: iconAlignment),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
testWidgets('Tonal icon default iconAlignment', (WidgetTester tester) async {
Widget buildWidget({ required TextDirection textDirection }) {
return MaterialApp(
@ -2645,6 +2703,7 @@ void main() {
style: FilledButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
iconAlignment: IconAlignment.end,
disabledIconColor: disabledIconColor,
),
onPressed: enabled ? () {} : null,
@ -2664,5 +2723,9 @@ void main() {
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -192,7 +192,7 @@ void main() {
});
});
testWidgets('Theme shadowColor', (WidgetTester tester) async {
testWidgets('FilledButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -256,4 +256,76 @@ void main() {
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
testWidgets('FilledButton.icon respects FilledButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
theme: ThemeData(
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(iconAlignment: iconAlignment),
),
),
home: Scaffold(
body: Center(
child: FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
await tester.pumpAndSettle();
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
testWidgets('Filled tonal button icon respects FilledButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
theme: ThemeData(
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(iconAlignment: iconAlignment),
),
),
home: Scaffold(
body: Center(
child: FilledButton.tonalIcon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
await tester.pumpAndSettle();
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -2471,7 +2471,7 @@ void main() {
expect(material.color, backgroundColor);
});
testWidgets('Default iconAlignment', (WidgetTester tester) async {
testWidgets('Default OutlinedButton icon alignment', (WidgetTester tester) async {
Widget buildWidget({ required TextDirection textDirection }) {
return MaterialApp(
home: Directionality(
@ -2506,7 +2506,7 @@ void main() {
expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge.
});
testWidgets('iconAlignment can be customized', (WidgetTester tester) async {
testWidgets('OutlinedButton icon alignment can be customized', (WidgetTester tester) async {
Widget buildWidget({
required TextDirection textDirection,
required IconAlignment iconAlignment,
@ -2583,6 +2583,35 @@ void main() {
expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge.
});
testWidgets('OutlinedButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
home: Center(
child: OutlinedButton.icon(
style: ButtonStyle(iconAlignment: iconAlignment),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
testWidgets("OutlinedButton.icon response doesn't hover when disabled", (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton.icon Focus');
@ -2763,6 +2792,7 @@ void main() {
style: OutlinedButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
iconAlignment: IconAlignment.end,
disabledIconColor: disabledIconColor,
),
onPressed: enabled ? () {} : null,
@ -2782,5 +2812,9 @@ void main() {
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -235,7 +235,7 @@ void main() {
});
});
testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material3 - OutlinedButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -306,7 +306,7 @@ void main() {
expect(material.shadowColor, shadowColor);
});
testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material2 - OutlinedButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -375,4 +375,40 @@ void main() {
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
testWidgets('OutlinedButton.icon alignment respects OutlinedButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
theme: ThemeData(
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(iconAlignment: iconAlignment),
),
),
home: Scaffold(
body: Center(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
await tester.pumpAndSettle();
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
});
}

View File

@ -2304,7 +2304,7 @@ void main() {
expect(material.color, backgroundColor);
});
testWidgets('Default iconAlignment', (WidgetTester tester) async {
testWidgets('Default TextButton icon alignment', (WidgetTester tester) async {
Widget buildWidget({ required TextDirection textDirection }) {
return MaterialApp(
home: Directionality(
@ -2339,7 +2339,7 @@ void main() {
expect(buttonTopRight.dx, iconTopRight.dx + 12.0); // 12.0 - padding between icon and button edge.
});
testWidgets('iconAlignment can be customized', (WidgetTester tester) async {
testWidgets('TextButton icon alignment can be customized', (WidgetTester tester) async {
Widget buildWidget({
required TextDirection textDirection,
required IconAlignment iconAlignment,
@ -2416,6 +2416,35 @@ void main() {
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
});
testWidgets('TextButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
home: Center(
child: TextButton.icon(
style: ButtonStyle(iconAlignment: iconAlignment),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 16.0);
});
testWidgets('treats a hovering stylus like a mouse', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
@ -2476,6 +2505,7 @@ void main() {
style: TextButton.styleFrom(
iconColor: iconColor,
iconSize: iconSize,
iconAlignment: IconAlignment.end,
disabledIconColor: disabledIconColor,
),
onPressed: enabled ? () {} : null,
@ -2495,5 +2525,9 @@ void main() {
// Test disabled button.
await tester.pumpWidget(buildButton(enabled: false));
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 16.0);
});
}

View File

@ -239,7 +239,7 @@ void main() {
});
});
testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material3 - TextButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -310,7 +310,7 @@ void main() {
expect(material.shadowColor, shadowColor);
});
testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async {
testWidgets('Material2 - TextButton repsects Theme shadowColor', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light();
const Color shadowColor = Color(0xff000001);
const Color overriddenColor = Color(0xff000002);
@ -379,4 +379,40 @@ void main() {
material = tester.widget<Material>(buttonMaterialFinder);
expect(material.shadowColor, shadowColor);
});
testWidgets('TextButton.icon respects TextButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async {
Widget buildButton({ IconAlignment? iconAlignment }) {
return MaterialApp(
theme: ThemeData(
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(iconAlignment: iconAlignment),
),
),
home: Scaffold(
body: Center(
child: TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('button'),
),
),
),
);
}
await tester.pumpWidget(buildButton());
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0);
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
await tester.pumpAndSettle();
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
expect(buttonTopRight.dx, iconTopRight.dx + 16.0);
});
}