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

[Material] Implement App Bar Theme (#26597)

This change creates an `AppBarTheme` to be used with `AppBar` widgets. This allows for users to theme their AppBars separately from their overall Theme if they choose.
parent 48817772
......@@ -18,6 +18,7 @@ export 'src/material/about.dart';
export 'src/material/animated_icons.dart';
export 'src/material/app.dart';
export 'src/material/app_bar.dart';
export 'src/material/app_bar_theme.dart';
export 'src/material/arc.dart';
export 'src/material/back_button.dart';
export 'src/material/bottom_app_bar.dart';
......
......@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'app_bar_theme.dart';
import 'back_button.dart';
import 'constants.dart';
import 'debug.dart';
......@@ -130,9 +131,14 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design app bar.
///
/// The arguments [elevation], [primary], [toolbarOpacity], [bottomOpacity]
/// and [automaticallyImplyLeading] must not be null. Additionally,
/// [elevation] must be non-negative.
/// The arguments [primary], [toolbarOpacity], [bottomOpacity]
/// 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.
///
/// Typically used in the [Scaffold.appBar] property.
AppBar({
......@@ -143,7 +149,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation = 4.0,
this.elevation,
this.backgroundColor,
this.brightness,
this.iconTheme,
......@@ -154,7 +160,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0,
}) : assert(automaticallyImplyLeading != null),
assert(elevation != null && elevation >= 0.0),
assert(elevation == null || elevation >= 0.0),
assert(primary != null),
assert(titleSpacing != null),
assert(toolbarOpacity != null),
......@@ -269,33 +275,39 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
///
/// This controls the size of the shadow below the app bar.
///
/// Defaults to 4, the appropriate elevation for app bars.
///
/// The value is non-negative.
///
/// 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;
/// 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 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.
......@@ -361,6 +373,8 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
}
class _AppBarState extends State<AppBar> {
static const double _defaultElevation = 4.0;
void _handleDrawerButton() {
Scaffold.of(context).openDrawer();
}
......@@ -374,6 +388,7 @@ class _AppBarState extends State<AppBar> {
assert(!widget.primary || debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData themeData = Theme.of(context);
final AppBarTheme appBarTheme = AppBarTheme.of(context);
final ScaffoldState scaffold = Scaffold.of(context, nullOk: true);
final ModalRoute<dynamic> parentRoute = ModalRoute.of(context);
......@@ -382,9 +397,15 @@ class _AppBarState extends State<AppBar> {
final bool canPop = parentRoute?.canPop ?? false;
final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;
IconThemeData appBarIconTheme = widget.iconTheme ?? themeData.primaryIconTheme;
TextStyle centerStyle = widget.textTheme?.title ?? themeData.primaryTextTheme.title;
TextStyle sideStyle = widget.textTheme?.body1 ?? themeData.primaryTextTheme.body1;
IconThemeData appBarIconTheme = widget.iconTheme
?? appBarTheme.iconTheme
?? themeData.primaryIconTheme;
TextStyle centerStyle = widget.textTheme?.title
?? appBarTheme.textTheme?.title
?? themeData.primaryTextTheme.title;
TextStyle sideStyle = widget.textTheme?.body1
?? appBarTheme.textTheme?.body1
?? themeData.primaryTextTheme.body1;
if (widget.toolbarOpacity != 1.0) {
final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity);
......@@ -517,18 +538,24 @@ class _AppBarState extends State<AppBar> {
],
);
}
final Brightness brightness = widget.brightness ?? themeData.primaryColorBrightness;
final Brightness brightness = widget.brightness
?? appBarTheme.brightness
?? themeData.primaryColorBrightness;
final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark;
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark;
return Semantics(
container: true,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: overlayStyle,
child: Material(
color: widget.backgroundColor ?? themeData.primaryColor,
elevation: widget.elevation,
color: widget.backgroundColor
?? appBarTheme.color
?? themeData.primaryColor,
elevation: widget.elevation
?? appBarTheme.elevation
?? _defaultElevation,
child: Semantics(
explicitChildNodes: true,
child: appBar,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'text_theme.dart';
import 'theme.dart';
/// Defines default property values for descendant [AppBar] widgets.
///
/// Descendant widgets obtain the current [AppBarTheme] object using
/// `AppBarTheme.of(context)`. Instances of [AppBarTheme] can be customized
/// with [AppBarTheme.copyWith].
///
/// Typically an [AppBarTheme] is specified as part of the overall [Theme] with
/// [ThemeData.appBarTheme].
///
/// All [AppBarTheme] properties are `null` by default. When null, the [AppBar]
/// will use the values from [ThemeData] if they exist, otherwise it will
/// provide its own defaults.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
class AppBarTheme extends Diagnosticable {
/// Creates a theme that can be used for [ThemeData.AppBarTheme].
const AppBarTheme({
this.brightness,
this.color,
this.elevation,
this.iconTheme,
this.textTheme,
});
/// Default value for [AppBar.brightness].
///
/// If null, [AppBar] uses [ThemeData.primaryColorBrightness].
final Brightness brightness;
/// Default value for [AppBar.color].
///
/// If null, [AppBar] uses [ThemeData.primaryColor].
final Color color;
/// Default value for [AppBar.elevation].
///
/// If null, [AppBar] uses a default value of 4.0.
final double elevation;
/// Default value for [AppBar.iconTheme].
///
/// If null, [AppBar] uses [ThemeData.primaryIconTheme].
final IconThemeData iconTheme;
/// Default value for [AppBar.textTheme].
///
/// If null, [AppBar] uses [ThemeData.primaryTextTheme].
final TextTheme textTheme;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
AppBarTheme copyWith({
Brightness brightness,
Color color,
double elevation,
IconThemeData iconTheme,
TextTheme textTheme,
}) {
return AppBarTheme(
brightness: brightness ?? this.brightness,
color: color ?? this.color,
elevation: elevation ?? this.elevation,
iconTheme: iconTheme ?? this.iconTheme,
textTheme: textTheme ?? this.textTheme,
);
}
/// The [ThemeData.appBarTheme] property of the ambient [Theme].
static AppBarTheme of(BuildContext context) {
return Theme.of(context).appBarTheme;
}
/// Linearly interpolate between two AppBar themes.
///
/// The argument `t` must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static AppBarTheme lerp(AppBarTheme a, AppBarTheme b, double t) {
assert(t != null);
return AppBarTheme(
brightness: t < 0.5 ? a?.brightness : b?.brightness,
color: Color.lerp(a?.color, b?.color, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
iconTheme: IconThemeData.lerp(a?.iconTheme, b?.iconTheme, t),
textTheme: TextTheme.lerp(a?.textTheme, b?.textTheme, t),
);
}
@override
int get hashCode {
return hashValues(
brightness,
color,
elevation,
iconTheme,
textTheme,
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final AppBarTheme typedOther = other;
return typedOther.brightness == brightness
&& typedOther.color == color
&& typedOther.elevation == elevation
&& typedOther.iconTheme == iconTheme
&& typedOther.textTheme == textTheme;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Brightness>('brightness', brightness, defaultValue: null));
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<TextTheme>('textTheme', textTheme, defaultValue: null));
}
}
......@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'app_bar_theme.dart';
import 'bottom_app_bar_theme.dart';
import 'button_theme.dart';
import 'chip_theme.dart';
......@@ -152,6 +153,7 @@ class ThemeData extends Diagnosticable {
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
PageTransitionsTheme pageTransitionsTheme,
AppBarTheme appBarTheme,
BottomAppBarTheme bottomAppBarTheme,
ColorScheme colorScheme,
DialogTheme dialogTheme,
......@@ -244,6 +246,7 @@ class ThemeData extends Diagnosticable {
valueIndicatorTextStyle: accentTextTheme.body2,
);
tabBarTheme ??= const TabBarTheme();
appBarTheme ??= const AppBarTheme();
bottomAppBarTheme ??= const BottomAppBarTheme();
chipTheme ??= ChipThemeData.fromDefaults(
secondaryColor: primaryColor,
......@@ -297,6 +300,7 @@ class ThemeData extends Diagnosticable {
platform: platform,
materialTapTargetSize: materialTapTargetSize,
pageTransitionsTheme: pageTransitionsTheme,
appBarTheme: appBarTheme,
bottomAppBarTheme: bottomAppBarTheme,
colorScheme: colorScheme,
dialogTheme: dialogTheme,
......@@ -359,6 +363,7 @@ class ThemeData extends Diagnosticable {
@required this.platform,
@required this.materialTapTargetSize,
@required this.pageTransitionsTheme,
@required this.appBarTheme,
@required this.bottomAppBarTheme,
@required this.colorScheme,
@required this.dialogTheme,
......@@ -406,6 +411,7 @@ class ThemeData extends Diagnosticable {
assert(platform != null),
assert(materialTapTargetSize != null),
assert(pageTransitionsTheme != null),
assert(appBarTheme != null),
assert(bottomAppBarTheme != null),
assert(colorScheme != null),
assert(dialogTheme != null),
......@@ -625,6 +631,10 @@ class ThemeData extends Diagnosticable {
/// builder is not found, a builder whose platform is null is used.
final PageTransitionsTheme pageTransitionsTheme;
/// A theme for customizing the color, elevation, brightness, iconTheme and
/// textTheme of [AppBar]s.
final AppBarTheme appBarTheme;
/// A theme for customizing the shape, elevation, and color of a [BottomAppBar].
final BottomAppBarTheme bottomAppBarTheme;
......@@ -702,6 +712,7 @@ class ThemeData extends Diagnosticable {
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
PageTransitionsTheme pageTransitionsTheme,
AppBarTheme appBarTheme,
BottomAppBarTheme bottomAppBarTheme,
ColorScheme colorScheme,
DialogTheme dialogTheme,
......@@ -753,6 +764,7 @@ class ThemeData extends Diagnosticable {
platform: platform ?? this.platform,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
appBarTheme: appBarTheme ?? this.appBarTheme,
bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme,
colorScheme: colorScheme ?? this.colorScheme,
dialogTheme: dialogTheme ?? this.dialogTheme,
......@@ -882,6 +894,7 @@ class ThemeData extends Diagnosticable {
platform: t < 0.5 ? a.platform : b.platform,
materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
pageTransitionsTheme: t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
bottomAppBarTheme: BottomAppBarTheme.lerp(a.bottomAppBarTheme, b.bottomAppBarTheme, t),
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
......@@ -941,6 +954,7 @@ class ThemeData extends Diagnosticable {
(otherData.platform == platform) &&
(otherData.materialTapTargetSize == materialTapTargetSize) &&
(otherData.pageTransitionsTheme == pageTransitionsTheme) &&
(otherData.appBarTheme == appBarTheme) &&
(otherData.bottomAppBarTheme == bottomAppBarTheme) &&
(otherData.colorScheme == colorScheme) &&
(otherData.dialogTheme == dialogTheme) &&
......@@ -1000,6 +1014,7 @@ class ThemeData extends Diagnosticable {
platform,
materialTapTargetSize,
pageTransitionsTheme,
appBarTheme,
bottomAppBarTheme,
colorScheme,
dialogTheme,
......@@ -1054,6 +1069,7 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme));
properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme));
properties.add(DiagnosticsProperty<BottomAppBarTheme>('bottomAppBarTheme', bottomAppBarTheme, defaultValue: defaultData.bottomAppBarTheme));
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme));
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('AppBarTheme copyWith, ==, hashCode basics', () {
expect(const AppBarTheme(), const AppBarTheme().copyWith());
expect(const AppBarTheme().hashCode, const AppBarTheme().copyWith().hashCode);
});
testWidgets('Passing no AppBarTheme returns defaults', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(appBar: AppBar()),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(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(text.style, Typography().englishLike.body1.merge(Typography().white.body1));
});
testWidgets('AppBar uses values from AppBarTheme', (WidgetTester tester) async {
final AppBarTheme appBarTheme = _appBarTheme();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(appBarTheme: appBarTheme),
home: Scaffold(appBar: AppBar(title: const Text('App Bar Title'),)),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(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(text.style, appBarTheme.textTheme.body1);
});
testWidgets('AppBar widget properties take priority over theme', (WidgetTester tester) async {
const Brightness brightness = Brightness.dark;
const Color color = Colors.orange;
const double elevation = 3.0;
const IconThemeData iconThemeData = IconThemeData(color: Colors.green);
const TextTheme textTheme = TextTheme(title: TextStyle(color: Colors.orange), body1: TextStyle(color: Colors.pink));
final ThemeData themeData = _themeData().copyWith(appBarTheme: _appBarTheme());
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(appBar: AppBar(
backgroundColor: color,
brightness: brightness,
elevation: elevation,
iconTheme: iconThemeData,
textTheme: textTheme,)
),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(tester);
final DefaultTextStyle text = _getAppBarText(tester);
expect(SystemChrome.latestStyle.statusBarBrightness, brightness);
expect(widget.color, color);
expect(widget.elevation, elevation);
expect(iconTheme.data, iconThemeData);
expect(text.style, textTheme.body1);
});
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()),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(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(text.style, appBarTheme.textTheme.body1);
});
testWidgets('ThemeData properties are used when no AppBarTheme is set', (WidgetTester tester) async {
final ThemeData themeData = _themeData();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(appBar: AppBar()),
));
final Material widget = _getAppBarMaterial(tester);
final IconTheme iconTheme = _getAppBarIconTheme(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(text.style, Typography().englishLike.body1.merge(Typography().white.body1).merge(themeData.primaryTextTheme.body1));
});
}
AppBarTheme _appBarTheme() {
const Brightness brightness = Brightness.light;
const Color color = Colors.lightBlue;
const double elevation = 6.0;
const IconThemeData iconThemeData = IconThemeData(color: Colors.black);
const TextTheme textTheme = TextTheme(body1: TextStyle(color: Colors.yellow));
return const AppBarTheme(
brightness: brightness,
color: color,
elevation: elevation,
iconTheme: iconThemeData,
textTheme: textTheme
);
}
ThemeData _themeData() {
return ThemeData(
primaryColor: Colors.purple,
brightness: Brightness.dark,
primaryIconTheme: const IconThemeData(color: Colors.green),
primaryTextTheme: const TextTheme(title: TextStyle(color: Colors.orange), body1: TextStyle(color: Colors.pink))
);
}
Material _getAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(
of: find.byType(AppBar),
matching: find.byType(Material),
),
);
}
IconTheme _getAppBarIconTheme(WidgetTester tester) {
return tester.widget<IconTheme>(
find.descendant(
of: find.byType(AppBar),
matching: find.byType(IconTheme),
),
);
}
DefaultTextStyle _getAppBarText(WidgetTester tester) {
return tester.widget<DefaultTextStyle>(
find.descendant(
of: find.byType(CustomSingleChildLayout),
matching: find.byType(DefaultTextStyle),
).first,
);
}
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