Unverified Commit fc6079a2 authored by rami-a's avatar rami-a Committed by GitHub

[Material] Add the ability to theme trailing app bar actions independently from leading (#28214)

* Allow app bar actions to be themed independently if needed

* Update SliverAppBar docs that were out of date

* Clarify fallback behavior in a comment

* Analyzer

* Address PR feedback

* Address PR feedback

* Put back checks on actions icon theme

* Address PR feedback

* Retrigger CI
parent c26a69e9
......@@ -135,10 +135,10 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// and [automaticallyImplyLeading] must not be null. Additionally, if
/// [elevation] is specified, it must be non-negative.
///
/// If [backgroundColor], [elevation], [brightness], [iconTheme], or
/// [textTheme] are null, their [AppBarTheme] values will be used. If the
/// corresponding [AppBarTheme] property is null, then the default specified
/// in the property's documentation will be used.
/// If [backgroundColor], [elevation], [brightness], [iconTheme],
/// [actionsIconTheme], or [textTheme] are null, then their [AppBarTheme]
/// values will be used. If the corresponding [AppBarTheme] property is null,
/// then the default specified in the property's documentation will be used.
///
/// Typically used in the [Scaffold.appBar] property.
AppBar({
......@@ -153,6 +153,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.backgroundColor,
this.brightness,
this.iconTheme,
this.actionsIconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
......@@ -280,7 +281,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
///
/// The value is non-negative.
///
/// If this property is null then [ThemeData.appBarTheme.elevation] is used,
/// If this property is null, then [ThemeData.appBarTheme.elevation] is used,
/// if that is also null, the default value is 4, the appropriate elevation
/// for app bars.
final double elevation;
......@@ -288,29 +289,38 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// The color to use for the app bar's material. Typically this should be set
/// along with [brightness], [iconTheme], [textTheme].
///
/// If this property is null then [ThemeData.appBarTheme.color] is used,
/// if that is also null then [ThemeData.primaryColor] is used.
/// If this property is null, then [ThemeData.appBarTheme.color] is used,
/// if that is also null, then [ThemeData.primaryColor] is used.
final Color backgroundColor;
/// The brightness of the app bar's material. Typically this is set along
/// with [backgroundColor], [iconTheme], [textTheme].
///
/// If this property is null then [ThemeData.appBarTheme.brightness] is used,
/// if that is also null then [ThemeData.primaryColorBrightness] is used.
/// If this property is null, then [ThemeData.appBarTheme.brightness] is used,
/// if that is also null, then [ThemeData.primaryColorBrightness] is used.
final Brightness brightness;
/// The color, opacity, and size to use for app bar icons. Typically this
/// is set along with [backgroundColor], [brightness], [textTheme].
///
/// If this property is null then [ThemeData.appBarTheme.iconTheme] is used,
/// if that is also null then [ThemeData.primaryIconTheme] is used.
/// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used,
/// if that is also null, then [ThemeData.primaryIconTheme] is used.
final IconThemeData iconTheme;
/// The color, opacity, and size to use for the icons that appear in the app
/// bar's [actions]. This should only be used when the [actions] should be
/// themed differently than the icon that appears in the app bar's [leading]
/// widget.
///
/// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is
/// used, if that is also null, then this falls back to [iconTheme].
final IconThemeData actionsIconTheme;
/// The typographic styles to use for text in the app bar. Typically this is
/// set along with [brightness] [backgroundColor], [iconTheme].
///
/// If this property is null then [ThemeData.appBarTheme.textTheme] is used,
/// if that is also null then [ThemeData.primaryTextTheme] is used.
/// If this property is null, then [ThemeData.appBarTheme.textTheme] is used,
/// if that is also null, then [ThemeData.primaryTextTheme] is used.
final TextTheme textTheme;
/// Whether this app bar is being displayed at the top of the screen.
......@@ -400,9 +410,12 @@ class _AppBarState extends State<AppBar> {
final bool canPop = parentRoute?.canPop ?? false;
final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;
IconThemeData appBarIconTheme = widget.iconTheme
IconThemeData overallIconTheme = widget.iconTheme
?? appBarTheme.iconTheme
?? themeData.primaryIconTheme;
IconThemeData actionsIconTheme = widget.actionsIconTheme
?? appBarTheme.actionsIconTheme
?? overallIconTheme;
TextStyle centerStyle = widget.textTheme?.title
?? appBarTheme.textTheme?.title
?? themeData.primaryTextTheme.title;
......@@ -416,8 +429,11 @@ class _AppBarState extends State<AppBar> {
centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
if (sideStyle?.color != null)
sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
appBarIconTheme = appBarIconTheme.copyWith(
opacity: opacity * (appBarIconTheme.opacity ?? 1.0)
overallIconTheme = overallIconTheme.copyWith(
opacity: opacity * (overallIconTheme.opacity ?? 1.0)
);
actionsIconTheme = actionsIconTheme.copyWith(
opacity: opacity * (actionsIconTheme.opacity ?? 1.0)
);
}
......@@ -479,6 +495,14 @@ 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 Widget toolbar = NavigationToolbar(
leading: leading,
middle: title,
......@@ -493,7 +517,7 @@ class _AppBarState extends State<AppBar> {
child: CustomSingleChildLayout(
delegate: const _ToolbarContainerLayout(),
child: IconTheme.merge(
data: appBarIconTheme,
data: overallIconTheme,
child: DefaultTextStyle(
style: sideStyle,
child: toolbar,
......@@ -634,6 +658,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.backgroundColor,
@required this.brightness,
@required this.iconTheme,
@required this.actionsIconTheme,
@required this.textTheme,
@required this.primary,
@required this.centerTitle,
......@@ -658,6 +683,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Color backgroundColor;
final Brightness brightness;
final IconThemeData iconTheme;
final IconThemeData actionsIconTheme;
final TextTheme textTheme;
final bool primary;
final bool centerTitle;
......@@ -716,6 +742,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
backgroundColor: backgroundColor,
brightness: brightness,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
textTheme: textTheme,
primary: primary,
centerTitle: centerTitle,
......@@ -740,6 +767,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| backgroundColor != oldDelegate.backgroundColor
|| brightness != oldDelegate.brightness
|| iconTheme != oldDelegate.iconTheme
|| actionsIconTheme != oldDelegate.actionsIconTheme
|| textTheme != oldDelegate.textTheme
|| primary != oldDelegate.primary
|| centerTitle != oldDelegate.centerTitle
......@@ -851,6 +879,7 @@ class SliverAppBar extends StatefulWidget {
this.backgroundColor,
this.brightness,
this.iconTheme,
this.actionsIconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
......@@ -944,7 +973,9 @@ class SliverAppBar extends StatefulWidget {
/// The z-coordinate at which to place this app bar when it is above other
/// content. This controls the size of the shadow below the app bar.
///
/// Defaults to 4, the appropriate elevation for app bars.
/// If this property is null, then [ThemeData.appBarTheme.elevation] is used,
/// if that is also null, the default value is 4, the appropriate elevation
/// for app bars.
///
/// If [forceElevated] is false, the elevation is ignored when the app bar has
/// no content underneath it. For example, if the app bar is [pinned] but no
......@@ -966,25 +997,37 @@ class SliverAppBar extends StatefulWidget {
/// The color to use for the app bar's material. Typically this should be set
/// along with [brightness], [iconTheme], [textTheme].
///
/// Defaults to [ThemeData.primaryColor].
/// If this property is null, then [ThemeData.appBarTheme.color] is used,
/// if that is also null, then [ThemeData.primaryColor] is used.
final Color backgroundColor;
/// The brightness of the app bar's material. Typically this is set along
/// with [backgroundColor], [iconTheme], [textTheme].
///
/// Defaults to [ThemeData.primaryColorBrightness].
/// If this property is null, then [ThemeData.appBarTheme.brightness] is used,
/// if that is also null, then [ThemeData.primaryColorBrightness] is used.
final Brightness brightness;
/// The color, opacity, and size to use for app bar icons. Typically this
/// is set along with [backgroundColor], [brightness], [textTheme].
///
/// Defaults to [ThemeData.primaryIconTheme].
/// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used,
/// if that is also null, then [ThemeData.primaryIconTheme] is used.
final IconThemeData iconTheme;
/// The color, opacity, and size to use for trailing app bar icons. This
/// should only be used when the trailing icons should be themed differently
/// than the leading icons.
///
/// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is
/// used, if that is also null, then this falls back to [iconTheme].
final IconThemeData actionsIconTheme;
/// The typographic styles to use for text in the app bar. Typically this is
/// set along with [brightness] [backgroundColor], [iconTheme].
///
/// Defaults to [ThemeData.primaryTextTheme].
/// If this property is null, then [ThemeData.appBarTheme.textTheme] is used,
/// if that is also null, then [ThemeData.primaryTextTheme] is used.
final TextTheme textTheme;
/// Whether this app bar is being displayed at the top of the screen.
......@@ -1148,6 +1191,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
backgroundColor: widget.backgroundColor,
brightness: widget.brightness,
iconTheme: widget.iconTheme,
actionsIconTheme: widget.actionsIconTheme,
textTheme: widget.textTheme,
primary: widget.primary,
centerTitle: widget.centerTitle,
......
......@@ -34,6 +34,7 @@ class AppBarTheme extends Diagnosticable {
this.color,
this.elevation,
this.iconTheme,
this.actionsIconTheme,
this.textTheme,
});
......@@ -57,6 +58,11 @@ class AppBarTheme extends Diagnosticable {
/// If null, [AppBar] uses [ThemeData.primaryIconTheme].
final IconThemeData iconTheme;
/// Default value for [AppBar.actionsIconTheme].
///
/// If null, [AppBar] uses [ThemeData.primaryIconTheme].
final IconThemeData actionsIconTheme;
/// Default value for [AppBar.textTheme].
///
/// If null, [AppBar] uses [ThemeData.primaryTextTheme].
......@@ -65,6 +71,7 @@ class AppBarTheme extends Diagnosticable {
/// Creates a copy of this object with the given fields replaced with the
/// new values.
AppBarTheme copyWith({
IconThemeData actionsIconTheme,
Brightness brightness,
Color color,
double elevation,
......@@ -76,6 +83,7 @@ class AppBarTheme extends Diagnosticable {
color: color ?? this.color,
elevation: elevation ?? this.elevation,
iconTheme: iconTheme ?? this.iconTheme,
actionsIconTheme: actionsIconTheme ?? this.actionsIconTheme,
textTheme: textTheme ?? this.textTheme,
);
}
......@@ -97,6 +105,7 @@ class AppBarTheme extends Diagnosticable {
color: Color.lerp(a?.color, b?.color, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
iconTheme: IconThemeData.lerp(a?.iconTheme, b?.iconTheme, t),
actionsIconTheme: IconThemeData.lerp(a?.actionsIconTheme, b?.actionsIconTheme, t),
textTheme: TextTheme.lerp(a?.textTheme, b?.textTheme, t),
);
}
......@@ -108,6 +117,7 @@ class AppBarTheme extends Diagnosticable {
color,
elevation,
iconTheme,
actionsIconTheme,
textTheme,
);
}
......@@ -123,6 +133,7 @@ class AppBarTheme extends Diagnosticable {
&& typedOther.color == color
&& typedOther.elevation == elevation
&& typedOther.iconTheme == iconTheme
&& typedOther.actionsIconTheme == actionsIconTheme
&& typedOther.textTheme == textTheme;
}
......@@ -133,6 +144,7 @@ class AppBarTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme, defaultValue: null));
properties.add(DiagnosticsProperty<IconThemeData>('actionsIconTheme', actionsIconTheme, defaultValue: null));
properties.add(DiagnosticsProperty<TextTheme>('textTheme', textTheme, defaultValue: null));
}
}
......@@ -15,17 +15,25 @@ void main() {
testWidgets('Passing no AppBarTheme returns defaults', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(appBar: AppBar()),
home: Scaffold(appBar: AppBar(
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), onPressed: () {})
],
)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
final RichText actionIconText = _getAppBarIconRichText(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, Brightness.dark);
expect(widget.color, Colors.blue);
expect(widget.elevation, 4.0);
expect(iconTheme.data, const IconThemeData(color: Colors.white));
expect(actionsIconTheme.data, const IconThemeData(color: Colors.white));
expect(actionIconText.text.style.color, Colors.white);
expect(text.style, Typography().englishLike.body1.merge(Typography().white.body1));
});
......@@ -34,17 +42,26 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(appBarTheme: appBarTheme),
home: Scaffold(appBar: AppBar(title: const Text('App Bar Title'),)),
home: Scaffold(appBar: AppBar(
title: const Text('App Bar Title'),
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), onPressed: () {})
],
)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
final RichText actionIconText = _getAppBarIconRichText(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, appBarTheme.brightness);
expect(widget.color, appBarTheme.color);
expect(widget.elevation, appBarTheme.elevation);
expect(iconTheme.data, appBarTheme.iconTheme);
expect(actionsIconTheme.data, appBarTheme.actionsIconTheme);
expect(actionIconText.text.style.color, appBarTheme.actionsIconTheme.color);
expect(text.style, appBarTheme.textTheme.body1);
});
......@@ -53,6 +70,7 @@ void main() {
const Color color = Colors.orange;
const double elevation = 3.0;
const IconThemeData iconThemeData = IconThemeData(color: Colors.green);
const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.lightBlue);
const TextTheme textTheme = TextTheme(title: TextStyle(color: Colors.orange), body1: TextStyle(color: Colors.pink));
final ThemeData themeData = _themeData().copyWith(appBarTheme: _appBarTheme());
......@@ -64,38 +82,76 @@ void main() {
brightness: brightness,
elevation: elevation,
iconTheme: iconThemeData,
textTheme: textTheme,)
),
actionsIconTheme: actionsIconThemeData,
textTheme: textTheme,
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), onPressed: () {})
],
)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
final RichText actionIconText = _getAppBarIconRichText(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, brightness);
expect(widget.color, color);
expect(widget.elevation, elevation);
expect(iconTheme.data, iconThemeData);
expect(actionsIconTheme.data, actionsIconThemeData);
expect(actionIconText.text.style.color, actionsIconThemeData.color);
expect(text.style, textTheme.body1);
});
testWidgets('AppBar icon color takes priority over everything', (WidgetTester tester) async {
const Color color = Colors.lime;
const IconThemeData iconThemeData = IconThemeData(color: Colors.green);
const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.lightBlue);
final ThemeData themeData = _themeData().copyWith(appBarTheme: _appBarTheme());
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(appBar: AppBar(
iconTheme: iconThemeData,
actionsIconTheme: actionsIconThemeData,
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), color: color, onPressed: () {})
],
)),
));
final RichText actionIconText = _getAppBarIconRichText(tester);
expect(actionIconText.text.style.color, color);
});
testWidgets('AppBarTheme properties take priority over ThemeData properties', (WidgetTester tester) async {
final AppBarTheme appBarTheme = _appBarTheme();
final ThemeData themeData = _themeData().copyWith(appBarTheme: _appBarTheme());
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(appBar: AppBar()),
home: Scaffold(appBar: AppBar(
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), onPressed: () {})
],
)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
final RichText actionIconText = _getAppBarIconRichText(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, appBarTheme.brightness);
expect(widget.color, appBarTheme.color);
expect(widget.elevation, appBarTheme.elevation);
expect(iconTheme.data, appBarTheme.iconTheme);
expect(actionsIconTheme.data, appBarTheme.actionsIconTheme);
expect(actionIconText.text.style.color, appBarTheme.actionsIconTheme.color);
expect(text.style, appBarTheme.textTheme.body1);
});
......@@ -104,17 +160,25 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(appBar: AppBar()),
home: Scaffold(appBar: AppBar(
actions: <Widget>[
IconButton(icon: const Icon(Icons.share), onPressed: () {})
],
)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester);
final RichText actionIconText = _getAppBarIconRichText(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, themeData.brightness);
expect(widget.color, themeData.primaryColor);
expect(widget.elevation, 4.0);
expect(iconTheme.data, themeData.primaryIconTheme);
expect(actionsIconTheme.data, themeData.primaryIconTheme);
expect(actionIconText.text.style.color, themeData.primaryIconTheme.color);
expect(text.style, Typography().englishLike.body1.merge(Typography().white.body1).merge(themeData.primaryTextTheme.body1));
});
}
......@@ -124,13 +188,15 @@ AppBarTheme _appBarTheme() {
const Color color = Colors.lightBlue;
const double elevation = 6.0;
const IconThemeData iconThemeData = IconThemeData(color: Colors.black);
const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.pink);
const TextTheme textTheme = TextTheme(body1: TextStyle(color: Colors.yellow));
return const AppBarTheme(
actionsIconTheme: actionsIconThemeData,
brightness: brightness,
color: color,
elevation: elevation,
iconTheme: iconThemeData,
textTheme: textTheme
textTheme: textTheme,
);
}
......@@ -157,10 +223,27 @@ IconTheme _getAppBarIconTheme(WidgetTester tester) {
find.descendant(
of: find.byType(AppBar),
matching: find.byType(IconTheme),
),
).first,
);
}
IconTheme _getAppBarActionsIconTheme(WidgetTester tester) {
return tester.widget<IconTheme>(
find.descendant(
of: find.byType(NavigationToolbar),
matching: find.byType(IconTheme),
).first,
);
}
RichText _getAppBarIconRichText(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.byType(Icon),
matching: find.byType(RichText),
).first,
);
}
DefaultTextStyle _getAppBarText(WidgetTester tester) {
return tester.widget<DefaultTextStyle>(
find.descendant(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment