Skip to content

Commit

Permalink
IconButtonTheme should be overridden by the AppBar/AppBarTheme's icon…
Browse files Browse the repository at this point in the history
…Theme and actionsIconTheme (#118216)
  • Loading branch information
QuncCccccc authored Jan 12, 2023
1 parent 5630d53 commit c7a3f0f
Show file tree
Hide file tree
Showing 3 changed files with 327 additions and 8 deletions.
73 changes: 65 additions & 8 deletions packages/flutter/lib/src/material/app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import 'package:flutter/widgets.dart';

import 'app_bar_theme.dart';
import 'back_button.dart';
import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'flexible_space_bar.dart';
import 'icon_button.dart';
import 'icon_button_theme.dart';
import 'icons.dart';
import 'material.dart';
import 'material_localizations.dart';
Expand Down Expand Up @@ -909,6 +911,7 @@ class _AppBarState extends State<AppBar> {
assert(!widget.primary || debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context);
final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context);
final AppBarTheme appBarTheme = AppBarTheme.of(context);
final AppBarTheme defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context);
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
Expand Down Expand Up @@ -1019,13 +1022,46 @@ class _AppBarState extends State<AppBar> {
}
}
if (leading != null) {
// Based on the Material Design 3 specs, the leading IconButton should have
// a size of 48x48, and a highlight size of 40x40. Users can also put other
// type of widgets on leading with the original config.
if (theme.useMaterial3) {
leading = ConstrainedBox(
if (leading is IconButton) {
final IconButtonThemeData effectiveIconButtonTheme;

// This comparison is to check if there is a custom [overallIconTheme]. If true, it means that no
// custom [overallIconTheme] is provided, so [iconButtonTheme] is applied. Otherwise, we generate
// a new [IconButtonThemeData] based on the values from [overallIconTheme]. If [iconButtonTheme] only
// has null values, the default [overallIconTheme] will be applied below by [IconTheme.merge]
if (overallIconTheme == defaults.iconTheme) {
effectiveIconButtonTheme = iconButtonTheme;
} else {
// The [IconButton.styleFrom] method is used to generate a correct [overlayColor] based on the [foregroundColor].
final ButtonStyle leadingIconButtonStyle = IconButton.styleFrom(
foregroundColor: overallIconTheme.color,
iconSize: overallIconTheme.size,
);

effectiveIconButtonTheme = IconButtonThemeData(
style: iconButtonTheme.style?.copyWith(
foregroundColor: leadingIconButtonStyle.foregroundColor,
overlayColor: leadingIconButtonStyle.overlayColor,
iconSize: leadingIconButtonStyle.iconSize,
)
);
}

leading = Center(
child: IconButtonTheme(
data: effectiveIconButtonTheme,
child: leading
)
);
}

// Based on the Material Design 3 specs, the leading IconButton should have
// a size of 48x48, and a highlight size of 40x40. Users can also put other
// type of widgets on leading with the original config.
leading = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: widget.leadingWidth ?? _kLeadingWidth),
child: leading is IconButton ? Center(child: leading) : leading,
child: leading,
);
} else {
leading = ConstrainedBox(
Expand Down Expand Up @@ -1101,9 +1137,30 @@ class _AppBarState extends State<AppBar> {

// Allow the trailing actions to have their own theme if necessary.
if (actions != null) {
actions = IconTheme.merge(
data: actionsIconTheme,
child: actions,
final IconButtonThemeData effectiveActionsIconButtonTheme;
if (actionsIconTheme == defaults.actionsIconTheme) {
effectiveActionsIconButtonTheme = iconButtonTheme;
} else {
final ButtonStyle actionsIconButtonStyle = IconButton.styleFrom(
foregroundColor: actionsIconTheme.color,
iconSize: actionsIconTheme.size,
);

effectiveActionsIconButtonTheme = IconButtonThemeData(
style: iconButtonTheme.style?.copyWith(
foregroundColor: actionsIconButtonStyle.foregroundColor,
overlayColor: actionsIconButtonStyle.overlayColor,
iconSize: actionsIconButtonStyle.iconSize,
)
);
}

actions = IconButtonTheme(
data: effectiveActionsIconButtonTheme,
child: IconTheme.merge(
data: actionsIconTheme,
child: actions,
),
);
}

Expand Down
112 changes: 112 additions & 0 deletions packages/flutter/test/material/app_bar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3195,6 +3195,118 @@ void main() {
expect(actionIconColor(), actionsIconColor);
expect(actionIconButtonColor(), actionsIconColor);
});

testWidgets('AppBar.iconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: Colors.red,
iconSize: 32.0,
),
),
useMaterial3: true,
);

const IconThemeData overallIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: overallIconTheme,
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('title'),
actions: <Widget>[
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);

Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
double? actionIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;

expect(leadingIconButtonColor(), Colors.yellow);
expect(leadingIconButtonSize(), 30.0);
expect(actionIconButtonColor(), Colors.yellow);
expect(actionIconButtonSize(), 30.0);
});

testWidgets('AppBar.actionsIconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: Colors.red,
iconSize: 32.0,
),
),
useMaterial3: true,
);

const IconThemeData actionsIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
actionsIconTheme: actionsIconTheme,
title: const Text('title'),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);

Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;
double? actionIconButtonSize() => iconStyle(tester, Icons.add)?.fontSize;

// The leading icon button uses the style in the IconButtonTheme because only actionsIconTheme is provided.
expect(leadingIconButtonColor(), Colors.red);
expect(leadingIconButtonSize(), 32.0);
expect(actionIconButtonColor(), Colors.yellow);
expect(actionIconButtonSize(), 30.0);
});

testWidgets('The foregroundColor property of the AppBar overrides any IconButtonTheme present in the theme - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: Colors.red,
),
),
useMaterial3: true,
);

await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
foregroundColor: Colors.purple,
title: const Text('title'),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);

Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color;

expect(leadingIconButtonColor(), Colors.purple);
expect(actionIconButtonColor(), Colors.purple);
});
});

testWidgets('AppBarTheme.backwardsCompatibility', (WidgetTester tester) async {
Expand Down
150 changes: 150 additions & 0 deletions packages/flutter/test/material/app_bar_theme_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,149 @@ void main() {
expect(appBar.surfaceTintColor, Colors.yellow);
});

testWidgets('AppBarTheme.iconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
const IconThemeData overallIconTheme = IconThemeData(color: Colors.yellow);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red),
),
appBarTheme: const AppBarTheme(iconTheme: overallIconTheme),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
title: const Text('Title'),
),
),
));

final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;

expect(leadingIconButtonColor, overallIconTheme.color);
expect(actionIconButtonColor, overallIconTheme.color);
});

testWidgets('AppBarTheme.iconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async {
const IconThemeData overallIconTheme = IconThemeData(size: 30.0);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(iconSize: 32.0),
),
appBarTheme: const AppBarTheme(iconTheme: overallIconTheme),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
title: const Text('Title'),
),
),
));

final double? leadingIconButtonSize = _iconStyle(tester, Icons.menu)?.fontSize;
final double? actionIconButtonSize = _iconStyle(tester, Icons.add)?.fontSize;

expect(leadingIconButtonSize, overallIconTheme.size);
expect(actionIconButtonSize, overallIconTheme.size);
});


testWidgets('AppBarTheme.actionsIconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
const IconThemeData actionsIconTheme = IconThemeData(color: Colors.yellow);
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red),
);

await tester.pumpWidget(MaterialApp(
theme: ThemeData(
iconButtonTheme: iconButtonTheme,
appBarTheme: const AppBarTheme(actionsIconTheme: actionsIconTheme),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
title: const Text('Title'),
),
),
));

final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;

expect(leadingIconButtonColor, Colors.red); // leading color should come from iconButtonTheme
expect(actionIconButtonColor, actionsIconTheme.color);
});

testWidgets('AppBarTheme.actionsIconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async {
const IconThemeData actionsIconTheme = IconThemeData(size: 30.0);
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
style: IconButton.styleFrom(iconSize: 32.0),
);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
iconButtonTheme: iconButtonTheme,
appBarTheme: const AppBarTheme(actionsIconTheme: actionsIconTheme),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {},),
actions: <Widget>[ IconButton(icon: const Icon(Icons.add), onPressed: () {},) ],
title: const Text('Title'),
),
),
));

final double? leadingIconButtonSize = _iconStyle(tester, Icons.menu)?.fontSize;
final double? actionIconButtonSize = _iconStyle(tester, Icons.add)?.fontSize;

expect(leadingIconButtonSize, 32.0); // The size of leading icon button should come from iconButtonTheme
expect(actionIconButtonSize, actionsIconTheme.size);
});

testWidgets('AppBarTheme.foregroundColor takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async {
final IconButtonThemeData iconButtonTheme = IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red),
);
const AppBarTheme appBarTheme = AppBarTheme(
foregroundColor: Colors.green,
);
final ThemeData themeData = ThemeData(
iconButtonTheme: iconButtonTheme,
appBarTheme: appBarTheme,
useMaterial3: true,
);

await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('title'),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);

final Color? leadingIconButtonColor = _iconStyle(tester, Icons.menu)?.color;
final Color? actionIconButtonColor = _iconStyle(tester, Icons.add)?.color;

expect(leadingIconButtonColor, appBarTheme.foregroundColor);
expect(actionIconButtonColor, appBarTheme.foregroundColor);
});

testWidgets('AppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async {
const double kTitleSpacing = 10;
await tester.pumpWidget(MaterialApp(
Expand Down Expand Up @@ -760,3 +903,10 @@ DefaultTextStyle _getAppBarText(WidgetTester tester) {
).first,
);
}

TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon).first, matching: find.byType(RichText)),
);
return iconRichText.text.style;
}

0 comments on commit c7a3f0f

Please sign in to comment.