Skip to content

Commit 1044239

Browse files
authored
Introduce iconAlignment for the buttons with icon (#137348)
Adds `iconAlignment` property to `ButtonStyleButton` widget. Fixes #89564 ### Example https://github.com/flutter/flutter/assets/13456345/1b5236c4-5c60-4915-b3c6-0a56c43f8a19
1 parent 82668f1 commit 1044239

11 files changed

Lines changed: 942 additions & 17 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Flutter code sample for using [ButtonStyleButton.iconAlignment] parameter.
8+
9+
void main() {
10+
runApp(const ButtonStyleButtonIconAlignmentApp());
11+
}
12+
13+
class ButtonStyleButtonIconAlignmentApp extends StatelessWidget {
14+
const ButtonStyleButtonIconAlignmentApp({super.key});
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return const MaterialApp(
19+
home: Scaffold(
20+
body: ButtonStyleButtonIconAlignmentExample(),
21+
),
22+
);
23+
}
24+
}
25+
26+
class ButtonStyleButtonIconAlignmentExample extends StatefulWidget {
27+
const ButtonStyleButtonIconAlignmentExample({super.key});
28+
29+
@override
30+
State<ButtonStyleButtonIconAlignmentExample> createState() => _ButtonStyleButtonIconAlignmentExampleState();
31+
}
32+
33+
class _ButtonStyleButtonIconAlignmentExampleState extends State<ButtonStyleButtonIconAlignmentExample> {
34+
TextDirection _textDirection = TextDirection.ltr;
35+
IconAlignment _iconAlignment = IconAlignment.start;
36+
37+
@override
38+
Widget build(BuildContext context) {
39+
return SafeArea(
40+
child: Directionality(
41+
key: const Key('Directionality'),
42+
textDirection: _textDirection,
43+
child: Center(
44+
child: Column(
45+
mainAxisSize: MainAxisSize.min,
46+
mainAxisAlignment: MainAxisAlignment.center,
47+
children: <Widget>[
48+
const Spacer(),
49+
OverflowBar(
50+
spacing: 10,
51+
overflowSpacing: 20,
52+
alignment: MainAxisAlignment.center,
53+
overflowAlignment: OverflowBarAlignment.center,
54+
children: <Widget>[
55+
ElevatedButton.icon(
56+
onPressed: () {},
57+
icon: const Icon(Icons.sunny),
58+
label: const Text('ElevatedButton'),
59+
iconAlignment: _iconAlignment,
60+
),
61+
FilledButton.icon(
62+
onPressed: () {},
63+
icon: const Icon(Icons.beach_access),
64+
label: const Text('FilledButton'),
65+
iconAlignment: _iconAlignment,
66+
),
67+
FilledButton.tonalIcon(
68+
onPressed: () {},
69+
icon: const Icon(Icons.cloud),
70+
label: const Text('FilledButton Tonal'),
71+
iconAlignment: _iconAlignment,
72+
),
73+
OutlinedButton.icon(
74+
onPressed: () {},
75+
icon: const Icon(Icons.light),
76+
label: const Text('OutlinedButton'),
77+
iconAlignment: _iconAlignment,
78+
),
79+
TextButton.icon(
80+
onPressed: () {},
81+
icon: const Icon(Icons.flight_takeoff),
82+
label: const Text('TextButton'),
83+
iconAlignment: _iconAlignment,
84+
),
85+
],
86+
),
87+
const Spacer(),
88+
OverflowBar(
89+
alignment: MainAxisAlignment.spaceEvenly,
90+
overflowAlignment: OverflowBarAlignment.center,
91+
spacing: 10,
92+
overflowSpacing: 10,
93+
children: <Widget>[
94+
Column(
95+
children: <Widget>[
96+
const Text('Icon alignment'),
97+
const SizedBox(height: 10),
98+
SegmentedButton<IconAlignment>(
99+
onSelectionChanged: (Set<IconAlignment> value) {
100+
setState(() {
101+
_iconAlignment = value.first;
102+
});
103+
},
104+
selected: <IconAlignment>{ _iconAlignment },
105+
segments: IconAlignment.values.map((IconAlignment iconAlignment) {
106+
return ButtonSegment<IconAlignment>(
107+
value: iconAlignment,
108+
label: Text(iconAlignment.name),
109+
);
110+
}).toList(),
111+
),
112+
],
113+
),
114+
Column(
115+
children: <Widget>[
116+
const Text('Text direction'),
117+
const SizedBox(height: 10),
118+
SegmentedButton<TextDirection>(
119+
onSelectionChanged: (Set<TextDirection> value) {
120+
setState(() {
121+
_textDirection = value.first;
122+
});
123+
},
124+
selected: <TextDirection>{ _textDirection },
125+
segments: const <ButtonSegment<TextDirection>>[
126+
ButtonSegment<TextDirection>(
127+
value: TextDirection.ltr,
128+
label: Text('LTR'),
129+
),
130+
ButtonSegment<TextDirection>(
131+
value: TextDirection.rtl,
132+
label: Text('RTL'),
133+
),
134+
],
135+
),
136+
],
137+
),
138+
],
139+
),
140+
const Spacer(),
141+
],
142+
),
143+
),
144+
),
145+
);
146+
}
147+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/button_style_button/button_style_button.icon_alignment.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('ButtonStyleButton.iconAlignment updates button icons alignment', (WidgetTester tester) async {
11+
await tester.pumpWidget(
12+
const example.ButtonStyleButtonIconAlignmentApp(),
13+
);
14+
15+
Finder findButtonMaterial(String text) {
16+
return find.ancestor(
17+
of: find.text(text),
18+
matching: find.byType(Material),
19+
).first;
20+
}
21+
22+
void expectedLeftIconPosition({
23+
required double iconOffset,
24+
required double textButtonIconOffset,
25+
}) {
26+
expect(
27+
tester.getTopLeft(findButtonMaterial('ElevatedButton')).dx,
28+
tester.getTopLeft(find.byIcon(Icons.sunny)).dx - iconOffset,
29+
);
30+
expect(
31+
tester.getTopLeft(findButtonMaterial('FilledButton')).dx,
32+
tester.getTopLeft(find.byIcon(Icons.beach_access)).dx - iconOffset,
33+
);
34+
expect(
35+
tester.getTopLeft(findButtonMaterial('FilledButton Tonal')).dx,
36+
tester.getTopLeft(find.byIcon(Icons.cloud)).dx - iconOffset,
37+
);
38+
expect(
39+
tester.getTopLeft(findButtonMaterial('OutlinedButton')).dx,
40+
tester.getTopLeft(find.byIcon(Icons.light)).dx - iconOffset,
41+
);
42+
expect(
43+
tester.getTopLeft(findButtonMaterial('TextButton')).dx,
44+
tester.getTopLeft(find.byIcon(Icons.flight_takeoff)).dx - textButtonIconOffset,
45+
);
46+
}
47+
48+
void expectedRightIconPosition({
49+
required double iconOffset,
50+
required double textButtonIconOffset,
51+
}) {
52+
expect(
53+
tester.getTopRight(findButtonMaterial('ElevatedButton')).dx,
54+
tester.getTopRight(find.byIcon(Icons.sunny)).dx + iconOffset,
55+
);
56+
expect(
57+
tester.getTopRight(findButtonMaterial('FilledButton')).dx,
58+
tester.getTopRight(find.byIcon(Icons.beach_access)).dx + iconOffset,
59+
);
60+
expect(
61+
tester.getTopRight(findButtonMaterial('FilledButton Tonal')).dx,
62+
tester.getTopRight(find.byIcon(Icons.cloud)).dx + iconOffset,
63+
);
64+
expect(
65+
tester.getTopRight(findButtonMaterial('OutlinedButton')).dx,
66+
tester.getTopRight(find.byIcon(Icons.light)).dx + iconOffset,
67+
);
68+
expect(
69+
tester.getTopRight(findButtonMaterial('TextButton')).dx,
70+
tester.getTopRight(find.byIcon(Icons.flight_takeoff)).dx + textButtonIconOffset,
71+
);
72+
}
73+
74+
// Test initial icon alignment in LTR.
75+
expectedLeftIconPosition(iconOffset: 16, textButtonIconOffset: 12);
76+
77+
// Update icon alignment to end.
78+
await tester.tap(find.text('end'));
79+
await tester.pumpAndSettle();
80+
81+
// Test icon alignment end in LTR.
82+
expectedRightIconPosition(iconOffset: 24, textButtonIconOffset: 16);
83+
84+
// Reset icon alignment to start.
85+
await tester.tap(find.text('start'));
86+
await tester.pumpAndSettle();
87+
88+
// Change text direction to RTL.
89+
await tester.tap(find.text('RTL'));
90+
await tester.pumpAndSettle();
91+
92+
// Test icon alignment start in LTR.
93+
expectedRightIconPosition(iconOffset: 16, textButtonIconOffset: 12);
94+
95+
// Update icon alignment to end.
96+
await tester.tap(find.text('end'));
97+
await tester.pumpAndSettle();
98+
99+
// Test icon alignment end in LTR.
100+
expectedLeftIconPosition(iconOffset: 24, textButtonIconOffset: 16);
101+
});
102+
}

packages/flutter/lib/src/material/button_style_button.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,46 @@ import 'package:flutter/widgets.dart';
1111
import 'button_style.dart';
1212
import 'colors.dart';
1313
import 'constants.dart';
14+
import 'elevated_button.dart';
15+
import 'filled_button.dart';
1416
import 'ink_well.dart';
1517
import 'material.dart';
1618
import 'material_state.dart';
19+
import 'outlined_button.dart';
20+
import 'text_button.dart';
1721
import 'theme_data.dart';
1822

23+
/// {@template flutter.material.ButtonStyleButton.iconAlignment}
24+
/// Determines the alignment of the icon within the widgets such as:
25+
/// - [ElevatedButton.icon],
26+
/// - [FilledButton.icon],
27+
/// - [FilledButton.tonalIcon].
28+
/// - [OutlinedButton.icon],
29+
/// - [TextButton.icon],
30+
///
31+
/// The effect of `iconAlignment` depends on [TextDirection]. If textDirection is
32+
/// [TextDirection.ltr] then [IconAlignment.start] and [IconAlignment.end] align the
33+
/// icon on the left or right respectively. If textDirection is [TextDirection.rtl] the
34+
/// the alignments are reversed.
35+
///
36+
/// Defaults to [IconAlignment.start].
37+
///
38+
/// {@tool dartpad}
39+
/// This sample demonstrates how to use `iconAlignment` to align the button icon to the start
40+
/// or the end of the button.
41+
///
42+
/// ** See code in examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart **
43+
/// {@end-tool}
44+
///
45+
/// {@endtemplate}
46+
enum IconAlignment {
47+
/// The icon is placed at the start of the button.
48+
start,
49+
50+
/// The icon is placed at the end of the button.
51+
end,
52+
}
53+
1954
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
2055
///
2156
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
@@ -44,6 +79,7 @@ abstract class ButtonStyleButton extends StatefulWidget {
4479
this.statesController,
4580
this.isSemanticButton = true,
4681
required this.child,
82+
this.iconAlignment = IconAlignment.start,
4783
});
4884

4985
/// Called when the button is tapped or otherwise activated.
@@ -117,6 +153,9 @@ abstract class ButtonStyleButton extends StatefulWidget {
117153
/// {@macro flutter.widgets.ProxyWidget.child}
118154
final Widget? child;
119155

156+
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
157+
final IconAlignment iconAlignment;
158+
120159
/// Returns a non-null [ButtonStyle] that's based primarily on the [Theme]'s
121160
/// [ThemeData.textTheme] and [ThemeData.colorScheme].
122161
///

packages/flutter/lib/src/material/elevated_button.dart

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class ElevatedButton extends ButtonStyleButton {
7474
super.clipBehavior,
7575
super.statesController,
7676
required super.child,
77+
super.iconAlignment,
7778
});
7879

7980
/// Create an elevated button from a pair of widgets that serve as the button's
@@ -83,6 +84,9 @@ class ElevatedButton extends ButtonStyleButton {
8384
/// at the start, and 16 at the end, with an 8 pixel gap in between.
8485
///
8586
/// If [icon] is null, will create an [ElevatedButton] instead.
87+
///
88+
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
89+
///
8690
factory ElevatedButton.icon({
8791
Key? key,
8892
required VoidCallback? onPressed,
@@ -96,6 +100,7 @@ class ElevatedButton extends ButtonStyleButton {
96100
MaterialStatesController? statesController,
97101
Widget? icon,
98102
required Widget label,
103+
IconAlignment iconAlignment = IconAlignment.start,
99104
}) {
100105
if (icon == null) {
101106
return ElevatedButton(
@@ -125,6 +130,7 @@ class ElevatedButton extends ButtonStyleButton {
125130
statesController: statesController,
126131
icon: icon,
127132
label: label,
133+
iconAlignment: iconAlignment,
128134
);
129135
}
130136

@@ -532,9 +538,15 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
532538
super.statesController,
533539
required Widget icon,
534540
required Widget label,
541+
super.iconAlignment,
535542
}) : super(
536543
autofocus: autofocus ?? false,
537-
child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style),
544+
child: _ElevatedButtonWithIconChild(
545+
icon: icon,
546+
label: label,
547+
buttonStyle: style,
548+
iconAlignment: iconAlignment,
549+
),
538550
);
539551

540552
@override
@@ -563,11 +575,17 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
563575
}
564576

565577
class _ElevatedButtonWithIconChild extends StatelessWidget {
566-
const _ElevatedButtonWithIconChild({ required this.label, required this.icon, required this.buttonStyle });
578+
const _ElevatedButtonWithIconChild({
579+
required this.label,
580+
required this.icon,
581+
required this.buttonStyle,
582+
required this.iconAlignment,
583+
});
567584

568585
final Widget label;
569586
final Widget icon;
570587
final ButtonStyle? buttonStyle;
588+
final IconAlignment iconAlignment;
571589

572590
@override
573591
Widget build(BuildContext context) {
@@ -576,7 +594,9 @@ class _ElevatedButtonWithIconChild extends StatelessWidget {
576594
final double gap = lerpDouble(8, 4, scale)!;
577595
return Row(
578596
mainAxisSize: MainAxisSize.min,
579-
children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
597+
children: iconAlignment == IconAlignment.start
598+
? <Widget>[icon, SizedBox(width: gap), Flexible(child: label)]
599+
: <Widget>[Flexible(child: label), SizedBox(width: gap), icon],
580600
);
581601
}
582602
}

0 commit comments

Comments
 (0)