diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 949f065e5d..2fd0d6c1a9 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -21,6 +21,7 @@ export 'src/material/app_bar.dart'; export 'src/material/arc.dart'; export 'src/material/back_button.dart'; export 'src/material/bottom_app_bar.dart'; +export 'src/material/bottom_app_bar_theme.dart'; export 'src/material/bottom_navigation_bar.dart'; export 'src/material/bottom_sheet.dart'; export 'src/material/button.dart'; diff --git a/packages/flutter/lib/src/material/bottom_app_bar.dart b/packages/flutter/lib/src/material/bottom_app_bar.dart index b334a2785f..f93124acda 100644 --- a/packages/flutter/lib/src/material/bottom_app_bar.dart +++ b/packages/flutter/lib/src/material/bottom_app_bar.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'bottom_app_bar_theme.dart'; import 'material.dart'; import 'scaffold.dart'; import 'theme.dart'; @@ -41,19 +42,23 @@ import 'theme.dart'; class BottomAppBar extends StatefulWidget { /// Creates a bottom application bar. /// - /// The [color], [elevation], and [clipBehavior] arguments must not be null. + /// The [clipBehavior] argument must not be null. /// Additionally, [elevation] must be non-negative. + /// + /// If [color], [elevation], or [shape] are null, their [BottomAppBarTheme] values will be used. + /// If the corresponding [BottomAppBarTheme] property is null, then the default + /// specified in the property's documentation will be used. const BottomAppBar({ Key key, this.color, - this.elevation = 8.0, + this.elevation, this.shape, this.clipBehavior = Clip.none, this.notchMargin = 4.0, this.child, - }) : assert(elevation != null), - assert(elevation >= 0.0), - assert(clipBehavior != null), + }) : assert(clipBehavior != null), + assert(elevation == null || elevation >= 0.0), + assert(notchMargin != null), super(key: key); /// The widget below this widget in the tree. @@ -66,7 +71,8 @@ class BottomAppBar extends StatefulWidget { /// The bottom app bar's background color. /// - /// When null defaults to [ThemeData.bottomAppBarColor]. + /// If this property is null then [ThemeData.bottomAppBarTheme.color] is used, + /// if that's null then [ThemeData.bottomAppBarColor] is used. final Color color; /// The z-coordinate at which to place this bottom app bar relative to its @@ -75,12 +81,14 @@ class BottomAppBar extends StatefulWidget { /// This controls the size of the shadow below the bottom app bar. The /// value is non-negative. /// - /// Defaults to 8, the appropriate elevation for bottom app bars. + /// If this property is null then [ThemeData.bottomAppBarTheme.elevation] is used, + /// if that's null, the default value is 8. final double elevation; /// The notch that is made for the floating action button. /// - /// If null the bottom app bar will be rectangular with no notch. + /// If this property is null then [ThemeData.bottomAppBarTheme.shape] is used, + /// if that's null then the shape will be rectangular with no notch. final NotchedShape shape; /// {@macro flutter.widgets.Clip} @@ -98,6 +106,7 @@ class BottomAppBar extends StatefulWidget { class _BottomAppBarState extends State { ValueListenable geometryListenable; + static const double _defaultElevation = 8.0; @override void didChangeDependencies() { @@ -107,17 +116,20 @@ class _BottomAppBarState extends State { @override Widget build(BuildContext context) { - final CustomClipper clipper = widget.shape != null + final ThemeData theme = Theme.of(context); + final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context); + final NotchedShape notchedShape = widget.shape ?? babTheme.shape; + final CustomClipper clipper = notchedShape != null ? _BottomAppBarClipper( geometry: geometryListenable, - shape: widget.shape, + shape: notchedShape, notchMargin: widget.notchMargin, ) : const ShapeBorderClipper(shape: RoundedRectangleBorder()); return PhysicalShape( clipper: clipper, - elevation: widget.elevation, - color: widget.color ?? Theme.of(context).bottomAppBarColor, + elevation: widget.elevation ?? babTheme.elevation ?? _defaultElevation, + color: widget.color ?? babTheme.color ?? theme.bottomAppBarColor, clipBehavior: widget.clipBehavior, child: Material( type: MaterialType.transparency, diff --git a/packages/flutter/lib/src/material/bottom_app_bar_theme.dart b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart new file mode 100644 index 0000000000..f3f788aa66 --- /dev/null +++ b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart @@ -0,0 +1,108 @@ +// Copyright 2018 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 'theme.dart'; + +/// Defines default property values for descendant [BottomAppBar] widgets. +/// +/// Descendant widgets obtain the current [BottomAppBarTheme] object using +/// `BottomAppBarTheme.of(context)`. Instances of [BottomAppBarTheme] can be +/// customized with [BottomAppBarTheme.copyWith]. +/// +/// Typically a [BottomAppBarTheme] is specified as part of the overall [Theme] +/// with [ThemeData.bottomAppBarTheme]. +/// +/// All [BottomAppBarTheme] properties are `null` by default. When null, the +/// [BottomAppBar] constructor provides defaults. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +class BottomAppBarTheme extends Diagnosticable { + /// Creates a theme that can be used for [ThemeData.BottomAppBarTheme]. + const BottomAppBarTheme({ + this.color, + this.elevation, + this.shape, + }); + + /// Default value for [BottomAppBar.color]. + /// + /// If null, [BottomAppBar] uses [ThemeData.bottomAppBarColor]. + final Color color; + + /// Default value for [BottomAppBar.elevation]. + final double elevation; + + /// Default value for [BottomAppBar.shape]. + final NotchedShape shape; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + BottomAppBarTheme copyWith({ + Color color, + double elevation, + NotchedShape shape, + }) { + return BottomAppBarTheme( + color: color ?? this.color, + elevation: elevation ?? this.elevation, + shape: shape ?? this.shape, + ); + } + + /// The [ThemeData.bottomAppBarTheme] property of the ambient [Theme]. + static BottomAppBarTheme of(BuildContext context) { + return Theme.of(context).bottomAppBarTheme; + } + + /// Linearly interpolate between two BAB themes. + /// + /// The argument `t` must not be null. + /// + /// {@macro dart.ui.shadow.lerp} + static BottomAppBarTheme lerp(BottomAppBarTheme a, BottomAppBarTheme b, double t) { + assert(t != null); + return BottomAppBarTheme( + color: Color.lerp(a?.color, b?.color, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), + shape: t < 0.5 ? a?.shape : b?.shape, + ); + } + + @override + int get hashCode { + return hashValues( + color, + elevation, + shape, + ); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final BottomAppBarTheme typedOther = other; + return typedOther.color == color + && typedOther.elevation == elevation + && typedOther.shape == shape; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('color', color, defaultValue: null)); + properties.add(DiagnosticsProperty('elevation', elevation, defaultValue: null)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + } +} diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 580256f21c..e86e6bae94 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'bottom_app_bar_theme.dart'; import 'button_theme.dart'; import 'chip_theme.dart'; import 'color_scheme.dart'; @@ -151,6 +152,7 @@ class ThemeData extends Diagnosticable { TargetPlatform platform, MaterialTapTargetSize materialTapTargetSize, PageTransitionsTheme pageTransitionsTheme, + BottomAppBarTheme bottomAppBarTheme, ColorScheme colorScheme, DialogTheme dialogTheme, Typography typography, @@ -242,6 +244,7 @@ class ThemeData extends Diagnosticable { valueIndicatorTextStyle: accentTextTheme.body2, ); tabBarTheme ??= const TabBarTheme(); + bottomAppBarTheme ??= const BottomAppBarTheme(); chipTheme ??= ChipThemeData.fromDefaults( secondaryColor: primaryColor, brightness: brightness, @@ -294,6 +297,7 @@ class ThemeData extends Diagnosticable { platform: platform, materialTapTargetSize: materialTapTargetSize, pageTransitionsTheme: pageTransitionsTheme, + bottomAppBarTheme: bottomAppBarTheme, colorScheme: colorScheme, dialogTheme: dialogTheme, typography: typography, @@ -355,6 +359,7 @@ class ThemeData extends Diagnosticable { @required this.platform, @required this.materialTapTargetSize, @required this.pageTransitionsTheme, + @required this.bottomAppBarTheme, @required this.colorScheme, @required this.dialogTheme, @required this.typography, @@ -401,6 +406,7 @@ class ThemeData extends Diagnosticable { assert(platform != null), assert(materialTapTargetSize != null), assert(pageTransitionsTheme != null), + assert(bottomAppBarTheme != null), assert(colorScheme != null), assert(dialogTheme != null), assert(typography != null); @@ -619,6 +625,9 @@ class ThemeData extends Diagnosticable { /// builder is not found, a builder whose platform is null is used. final PageTransitionsTheme pageTransitionsTheme; + /// A theme for customizing the shape, elevation, and color of a [BottomAppBar]. + final BottomAppBarTheme bottomAppBarTheme; + /// A set of thirteen colors that can be used to configure the /// color properties of most components. /// @@ -693,6 +702,7 @@ class ThemeData extends Diagnosticable { TargetPlatform platform, MaterialTapTargetSize materialTapTargetSize, PageTransitionsTheme pageTransitionsTheme, + BottomAppBarTheme bottomAppBarTheme, ColorScheme colorScheme, DialogTheme dialogTheme, Typography typography, @@ -743,6 +753,7 @@ class ThemeData extends Diagnosticable { platform: platform ?? this.platform, materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize, pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme, + bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme, colorScheme: colorScheme ?? this.colorScheme, dialogTheme: dialogTheme ?? this.dialogTheme, typography: typography ?? this.typography, @@ -871,6 +882,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, + bottomAppBarTheme: BottomAppBarTheme.lerp(a.bottomAppBarTheme, b.bottomAppBarTheme, t), colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t), dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t), typography: Typography.lerp(a.typography, b.typography, t), @@ -929,6 +941,7 @@ class ThemeData extends Diagnosticable { (otherData.platform == platform) && (otherData.materialTapTargetSize == materialTapTargetSize) && (otherData.pageTransitionsTheme == pageTransitionsTheme) && + (otherData.bottomAppBarTheme == bottomAppBarTheme) && (otherData.colorScheme == colorScheme) && (otherData.dialogTheme == dialogTheme) && (otherData.typography == typography) && @@ -987,6 +1000,7 @@ class ThemeData extends Diagnosticable { platform, materialTapTargetSize, pageTransitionsTheme, + bottomAppBarTheme, colorScheme, dialogTheme, typography, @@ -1040,6 +1054,7 @@ class ThemeData extends Diagnosticable { properties.add(DiagnosticsProperty('chipTheme', chipTheme)); properties.add(DiagnosticsProperty('materialTapTargetSize', materialTapTargetSize)); properties.add(DiagnosticsProperty('pageTransitionsTheme', pageTransitionsTheme)); + properties.add(DiagnosticsProperty('bottomAppBarTheme', bottomAppBarTheme, defaultValue: defaultData.bottomAppBarTheme)); properties.add(DiagnosticsProperty('colorScheme', colorScheme, defaultValue: defaultData.colorScheme)); properties.add(DiagnosticsProperty('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme)); properties.add(DiagnosticsProperty('typography', typography, defaultValue: defaultData.typography)); diff --git a/packages/flutter/test/material/bottom_app_bar_theme_test.dart b/packages/flutter/test/material/bottom_app_bar_theme_test.dart new file mode 100644 index 0000000000..238c894ebd --- /dev/null +++ b/packages/flutter/test/material/bottom_app_bar_theme_test.dart @@ -0,0 +1,133 @@ +// Copyright 2018 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:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('BAB theme overrides color', (WidgetTester tester) async { + const Color themedColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme(color: themedColor); + + await tester.pumpWidget(_withTheme(theme)); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, themedColor); + }); + + testWidgets('BAB color - Widget', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const Color babColor = Colors.pink; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(bottomAppBarTheme: theme, bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar(color: babColor)), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babColor); + }); + + testWidgets('BAB color - BabTheme', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(bottomAppBarTheme: theme, bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babThemeColor); + }); + + testWidgets('BAB color - Theme', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, themeColor); + }); + + testWidgets('BAB color - Default', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(), + home: const Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + + expect(widget.color, Colors.white); + }); + + testWidgets('BAB theme customizes shape', (WidgetTester tester) async { + const BottomAppBarTheme theme = BottomAppBarTheme( + color: Colors.white30, + shape: CircularNotchedRectangle(), + elevation: 1.0, + ); + + await tester.pumpWidget(_withTheme(theme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'), + skip: !Platform.isLinux, + ); + }); + + testWidgets('BAB theme does not affect defaults', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + + expect(widget.color, Colors.white); + expect(widget.elevation, equals(8.0)); + }); +} + +PhysicalShape _getBabRenderObject(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(BottomAppBar), + matching: find.byType(PhysicalShape), + ), + ); +} + +final Key _painterKey = UniqueKey(); + +Widget _withTheme(BottomAppBarTheme theme) { + return MaterialApp( + theme: ThemeData(bottomAppBarTheme: theme), + home: Scaffold( + floatingActionButton: const FloatingActionButton(onPressed: null), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: RepaintBoundary( + key: _painterKey, + child: BottomAppBar( + child: Row( + children: const [ + Icon(Icons.add), + Expanded(child: SizedBox()), + Icon(Icons.add), + ], + ), + ), + ) + ), + ); +}