Skip to content

Commit c7a3f0f

Browse files
authored
IconButtonTheme should be overridden by the AppBar/AppBarTheme's iconTheme and actionsIconTheme (#118216)
1 parent 5630d53 commit c7a3f0f

File tree

3 files changed

+327
-8
lines changed

3 files changed

+327
-8
lines changed

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

+65-8
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import 'package:flutter/widgets.dart';
1111

1212
import 'app_bar_theme.dart';
1313
import 'back_button.dart';
14+
import 'button_style.dart';
1415
import 'color_scheme.dart';
1516
import 'colors.dart';
1617
import 'constants.dart';
1718
import 'debug.dart';
1819
import 'flexible_space_bar.dart';
1920
import 'icon_button.dart';
21+
import 'icon_button_theme.dart';
2022
import 'icons.dart';
2123
import 'material.dart';
2224
import 'material_localizations.dart';
@@ -909,6 +911,7 @@ class _AppBarState extends State<AppBar> {
909911
assert(!widget.primary || debugCheckHasMediaQuery(context));
910912
assert(debugCheckHasMaterialLocalizations(context));
911913
final ThemeData theme = Theme.of(context);
914+
final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context);
912915
final AppBarTheme appBarTheme = AppBarTheme.of(context);
913916
final AppBarTheme defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context);
914917
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
@@ -1019,13 +1022,46 @@ class _AppBarState extends State<AppBar> {
10191022
}
10201023
}
10211024
if (leading != null) {
1022-
// Based on the Material Design 3 specs, the leading IconButton should have
1023-
// a size of 48x48, and a highlight size of 40x40. Users can also put other
1024-
// type of widgets on leading with the original config.
10251025
if (theme.useMaterial3) {
1026-
leading = ConstrainedBox(
1026+
if (leading is IconButton) {
1027+
final IconButtonThemeData effectiveIconButtonTheme;
1028+
1029+
// This comparison is to check if there is a custom [overallIconTheme]. If true, it means that no
1030+
// custom [overallIconTheme] is provided, so [iconButtonTheme] is applied. Otherwise, we generate
1031+
// a new [IconButtonThemeData] based on the values from [overallIconTheme]. If [iconButtonTheme] only
1032+
// has null values, the default [overallIconTheme] will be applied below by [IconTheme.merge]
1033+
if (overallIconTheme == defaults.iconTheme) {
1034+
effectiveIconButtonTheme = iconButtonTheme;
1035+
} else {
1036+
// The [IconButton.styleFrom] method is used to generate a correct [overlayColor] based on the [foregroundColor].
1037+
final ButtonStyle leadingIconButtonStyle = IconButton.styleFrom(
1038+
foregroundColor: overallIconTheme.color,
1039+
iconSize: overallIconTheme.size,
1040+
);
1041+
1042+
effectiveIconButtonTheme = IconButtonThemeData(
1043+
style: iconButtonTheme.style?.copyWith(
1044+
foregroundColor: leadingIconButtonStyle.foregroundColor,
1045+
overlayColor: leadingIconButtonStyle.overlayColor,
1046+
iconSize: leadingIconButtonStyle.iconSize,
1047+
)
1048+
);
1049+
}
1050+
1051+
leading = Center(
1052+
child: IconButtonTheme(
1053+
data: effectiveIconButtonTheme,
1054+
child: leading
1055+
)
1056+
);
1057+
}
1058+
1059+
// Based on the Material Design 3 specs, the leading IconButton should have
1060+
// a size of 48x48, and a highlight size of 40x40. Users can also put other
1061+
// type of widgets on leading with the original config.
1062+
leading = ConstrainedBox(
10271063
constraints: BoxConstraints.tightFor(width: widget.leadingWidth ?? _kLeadingWidth),
1028-
child: leading is IconButton ? Center(child: leading) : leading,
1064+
child: leading,
10291065
);
10301066
} else {
10311067
leading = ConstrainedBox(
@@ -1101,9 +1137,30 @@ class _AppBarState extends State<AppBar> {
11011137

11021138
// Allow the trailing actions to have their own theme if necessary.
11031139
if (actions != null) {
1104-
actions = IconTheme.merge(
1105-
data: actionsIconTheme,
1106-
child: actions,
1140+
final IconButtonThemeData effectiveActionsIconButtonTheme;
1141+
if (actionsIconTheme == defaults.actionsIconTheme) {
1142+
effectiveActionsIconButtonTheme = iconButtonTheme;
1143+
} else {
1144+
final ButtonStyle actionsIconButtonStyle = IconButton.styleFrom(
1145+
foregroundColor: actionsIconTheme.color,
1146+
iconSize: actionsIconTheme.size,
1147+
);
1148+
1149+
effectiveActionsIconButtonTheme = IconButtonThemeData(
1150+
style: iconButtonTheme.style?.copyWith(
1151+
foregroundColor: actionsIconButtonStyle.foregroundColor,
1152+
overlayColor: actionsIconButtonStyle.overlayColor,
1153+
iconSize: actionsIconButtonStyle.iconSize,
1154+
)
1155+
);
1156+
}
1157+
1158+
actions = IconButtonTheme(
1159+
data: effectiveActionsIconButtonTheme,
1160+
child: IconTheme.merge(
1161+
data: actionsIconTheme,
1162+
child: actions,
1163+
),
11071164
);
11081165
}
11091166

packages/flutter/test/material/app_bar_test.dart

+112
Original file line numberDiff line numberDiff line change
@@ -3195,6 +3195,118 @@ void main() {
31953195
expect(actionIconColor(), actionsIconColor);
31963196
expect(actionIconButtonColor(), actionsIconColor);
31973197
});
3198+
3199+
testWidgets('AppBar.iconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
3200+
final ThemeData themeData = ThemeData(
3201+
iconButtonTheme: IconButtonThemeData(
3202+
style: IconButton.styleFrom(
3203+
foregroundColor: Colors.red,
3204+
iconSize: 32.0,
3205+
),
3206+
),
3207+
useMaterial3: true,
3208+
);
3209+
3210+
const IconThemeData overallIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
3211+
await tester.pumpWidget(
3212+
MaterialApp(
3213+
theme: themeData,
3214+
home: Scaffold(
3215+
appBar: AppBar(
3216+
iconTheme: overallIconTheme,
3217+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
3218+
title: const Text('title'),
3219+
actions: <Widget>[
3220+
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
3221+
],
3222+
),
3223+
),
3224+
),
3225+
);
3226+
3227+
Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
3228+
double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;
3229+
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
3230+
double? actionIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;
3231+
3232+
expect(leadingIconButtonColor(), Colors.yellow);
3233+
expect(leadingIconButtonSize(), 30.0);
3234+
expect(actionIconButtonColor(), Colors.yellow);
3235+
expect(actionIconButtonSize(), 30.0);
3236+
});
3237+
3238+
testWidgets('AppBar.actionsIconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
3239+
final ThemeData themeData = ThemeData(
3240+
iconButtonTheme: IconButtonThemeData(
3241+
style: IconButton.styleFrom(
3242+
foregroundColor: Colors.red,
3243+
iconSize: 32.0,
3244+
),
3245+
),
3246+
useMaterial3: true,
3247+
);
3248+
3249+
const IconThemeData actionsIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
3250+
await tester.pumpWidget(
3251+
MaterialApp(
3252+
theme: themeData,
3253+
home: Scaffold(
3254+
appBar: AppBar(
3255+
actionsIconTheme: actionsIconTheme,
3256+
title: const Text('title'),
3257+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
3258+
actions: <Widget>[
3259+
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
3260+
],
3261+
),
3262+
),
3263+
),
3264+
);
3265+
3266+
Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
3267+
double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;
3268+
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
3269+
double? actionIconButtonSize() => iconStyle(tester, Icons.add)?.fontSize;
3270+
3271+
// The leading icon button uses the style in the IconButtonTheme because only actionsIconTheme is provided.
3272+
expect(leadingIconButtonColor(), Colors.red);
3273+
expect(leadingIconButtonSize(), 32.0);
3274+
expect(actionIconButtonColor(), Colors.yellow);
3275+
expect(actionIconButtonSize(), 30.0);
3276+
});
3277+
3278+
testWidgets('The foregroundColor property of the AppBar overrides any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
3279+
final ThemeData themeData = ThemeData(
3280+
iconButtonTheme: IconButtonThemeData(
3281+
style: IconButton.styleFrom(
3282+
foregroundColor: Colors.red,
3283+
),
3284+
),
3285+
useMaterial3: true,
3286+
);
3287+
3288+
await tester.pumpWidget(
3289+
MaterialApp(
3290+
theme: themeData,
3291+
home: Scaffold(
3292+
appBar: AppBar(
3293+
foregroundColor: Colors.purple,
3294+
title: const Text('title'),
3295+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
3296+
actions: <Widget>[
3297+
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
3298+
],
3299+
),
3300+
),
3301+
),
3302+
);
3303+
3304+
Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
3305+
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
3306+
3307+
expect(leadingIconButtonColor(), Colors.purple);
3308+
expect(actionIconButtonColor(), Colors.purple);
3309+
});
31983310
});
31993311

32003312
testWidgets('AppBarTheme.backwardsCompatibility', (WidgetTester tester) async {

packages/flutter/test/material/app_bar_theme_test.dart

+150
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,149 @@ void main() {
508508
expect(appBar.surfaceTintColor, Colors.yellow);
509509
});
510510

511+
testWidgets('AppBarTheme.iconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
512+
const IconThemeData overallIconTheme = IconThemeData(color: Colors.yellow);
513+
await tester.pumpWidget(MaterialApp(
514+
theme: ThemeData(
515+
iconButtonTheme: IconButtonThemeData(
516+
style: IconButton.styleFrom(foregroundColor: Colors.red),
517+
),
518+
appBarTheme: const AppBarTheme(iconTheme: overallIconTheme),
519+
useMaterial3: true,
520+
),
521+
home: Scaffold(
522+
appBar: AppBar(
523+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
524+
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
525+
title: const Text('Title'),
526+
),
527+
),
528+
));
529+
530+
final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
531+
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;
532+
533+
expect(leadingIconButtonColor, overallIconTheme.color);
534+
expect(actionIconButtonColor, overallIconTheme.color);
535+
});
536+
537+
testWidgets('AppBarTheme.iconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async {
538+
const IconThemeData overallIconTheme = IconThemeData(size: 30.0);
539+
await tester.pumpWidget(MaterialApp(
540+
theme: ThemeData(
541+
iconButtonTheme: IconButtonThemeData(
542+
style: IconButton.styleFrom(iconSize: 32.0),
543+
),
544+
appBarTheme: const AppBarTheme(iconTheme: overallIconTheme),
545+
useMaterial3: true,
546+
),
547+
home: Scaffold(
548+
appBar: AppBar(
549+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
550+
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
551+
title: const Text('Title'),
552+
),
553+
),
554+
));
555+
556+
final double? leadingIconButtonSize = _iconStyle(tester, Icons.menu)?.fontSize;
557+
final double? actionIconButtonSize = _iconStyle(tester, Icons.add)?.fontSize;
558+
559+
expect(leadingIconButtonSize, overallIconTheme.size);
560+
expect(actionIconButtonSize, overallIconTheme.size);
561+
});
562+
563+
564+
testWidgets('AppBarTheme.actionsIconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
565+
const IconThemeData actionsIconTheme = IconThemeData(color: Colors.yellow);
566+
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
567+
style: IconButton.styleFrom(foregroundColor: Colors.red),
568+
);
569+
570+
await tester.pumpWidget(MaterialApp(
571+
theme: ThemeData(
572+
iconButtonTheme: iconButtonTheme,
573+
appBarTheme: const AppBarTheme(actionsIconTheme: actionsIconTheme),
574+
useMaterial3: true,
575+
),
576+
home: Scaffold(
577+
appBar: AppBar(
578+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
579+
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
580+
title: const Text('Title'),
581+
),
582+
),
583+
));
584+
585+
final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
586+
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;
587+
588+
expect(leadingIconButtonColor, Colors.red); // leading color should come from iconButtonTheme
589+
expect(actionIconButtonColor, actionsIconTheme.color);
590+
});
591+
592+
testWidgets('AppBarTheme.actionsIconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async {
593+
const IconThemeData actionsIconTheme = IconThemeData(size: 30.0);
594+
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
595+
style: IconButton.styleFrom(iconSize: 32.0),
596+
);
597+
await tester.pumpWidget(MaterialApp(
598+
theme: ThemeData(
599+
iconButtonTheme: iconButtonTheme,
600+
appBarTheme: const AppBarTheme(actionsIconTheme: actionsIconTheme),
601+
useMaterial3: true,
602+
),
603+
home: Scaffold(
604+
appBar: AppBar(
605+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
606+
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
607+
title: const Text('Title'),
608+
),
609+
),
610+
));
611+
612+
final double? leadingIconButtonSize = _iconStyle(tester, Icons.menu)?.fontSize;
613+
final double? actionIconButtonSize = _iconStyle(tester, Icons.add)?.fontSize;
614+
615+
expect(leadingIconButtonSize, 32.0); // The size of leading icon button should come from iconButtonTheme
616+
expect(actionIconButtonSize, actionsIconTheme.size);
617+
});
618+
619+
testWidgets('AppBarTheme.foregroundColor takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
620+
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
621+
style: IconButton.styleFrom(foregroundColor: Colors.red),
622+
);
623+
const AppBarTheme appBarTheme = AppBarTheme(
624+
foregroundColor: Colors.green,
625+
);
626+
final ThemeData themeData = ThemeData(
627+
iconButtonTheme: iconButtonTheme,
628+
appBarTheme: appBarTheme,
629+
useMaterial3: true,
630+
);
631+
632+
await tester.pumpWidget(
633+
MaterialApp(
634+
theme: themeData,
635+
home: Scaffold(
636+
appBar: AppBar(
637+
title: const Text('title'),
638+
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
639+
actions: <Widget>[
640+
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
641+
],
642+
),
643+
),
644+
),
645+
);
646+
647+
final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
648+
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;
649+
650+
expect(leadingIconButtonColor, appBarTheme.foregroundColor);
651+
expect(actionIconButtonColor, appBarTheme.foregroundColor);
652+
});
653+
511654
testWidgets('AppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async {
512655
const double kTitleSpacing = 10;
513656
await tester.pumpWidget(MaterialApp(
@@ -760,3 +903,10 @@ DefaultTextStyle _getAppBarText(WidgetTester tester) {
760903
).first,
761904
);
762905
}
906+
907+
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
908+
final RichText iconRichText = tester.widget<RichText>(
909+
find.descendant(of: find.byIcon(icon).first, matching: find.byType(RichText)),
910+
);
911+
return iconRichText.text.style;
912+
}

0 commit comments

Comments
 (0)