diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index cc8b08154f3..e09a4e1d5b7 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -18,6 +18,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:gen_defaults/fab_template.dart'; +import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/typography_template.dart'; Map _readTokenFile(String fileName) { @@ -64,5 +65,6 @@ Future main(List args) async { tokens['colorsDark'] = _readTokenFile('color_dark.json'); FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile(); + NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile(); TypographyTemplate('$materialLib/typography.dart', tokens).updateFile(); } diff --git a/dev/tools/gen_defaults/data/shape.json b/dev/tools/gen_defaults/data/shape.json index b62deca9391..6c3893b3e45 100644 --- a/dev/tools/gen_defaults/data/shape.json +++ b/dev/tools/gen_defaults/data/shape.json @@ -33,6 +33,10 @@ "bottomRight": 0.0 }, + "md.sys.shape.corner.full": { + "family": "SHAPE_FAMILY_CIRCULAR" + }, + "md.sys.shape.corner.large": { "family": "SHAPE_FAMILY_ROUNDED_CORNERS", "topLeft": 16.0, diff --git a/dev/tools/gen_defaults/lib/navigation_bar_template.dart b/dev/tools/gen_defaults/lib/navigation_bar_template.dart new file mode 100644 index 00000000000..9f538d6b619 --- /dev/null +++ b/dev/tools/gen_defaults/lib/navigation_bar_template.dart @@ -0,0 +1,57 @@ +// Copyright 2014 The Flutter 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 'template.dart'; + +class NavigationBarTemplate extends TokenTemplate { + const NavigationBarTemplate(String fileName, Map tokens) : super(fileName, tokens); + + @override + String generate() => ''' +// Generated version ${tokens["version"]} +class _TokenDefaultsM3 extends NavigationBarThemeData { + _TokenDefaultsM3(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + height: ${tokens["md.comp.navigation-bar.container.height"]}, + elevation: ${elevation("md.comp.navigation-bar.container")}, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + // With Material 3, the NavigationBar uses an overlay blend for the + // default color regardless of light/dark mode. This should be handled + // in the Material widget based off of elevation, but for now we will do + // it here in the defaults. + @override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.${color("md.comp.navigation-bar.container")}, _colors.primary, ${elevation("md.comp.navigation-bar.container")}); + + @override MaterialStateProperty? get iconTheme { + return MaterialStateProperty.resolveWith((Set states) { + return IconThemeData( + size: ${tokens["md.comp.navigation-bar.icon.size"]}, + color: states.contains(MaterialState.selected) + ? _colors.${color("md.comp.navigation-bar.active.icon")} + : _colors.${color("md.comp.navigation-bar.inactive.icon")}, + ); + }); + } + + @override Color? get indicatorColor => _colors.${color("md.comp.navigation-bar.active-indicator")}; + @override ShapeBorder? get indicatorShape => ${shape("md.comp.navigation-bar.active-indicator")}; + + @override MaterialStateProperty? get labelTextStyle { + return MaterialStateProperty.resolveWith((Set states) { + final TextStyle style = _theme.textTheme.${textStyle("md.comp.navigation-bar.label-text")}!; + return style.apply(color: states.contains(MaterialState.selected) + ? _colors.${color("md.comp.navigation-bar.active.label-text")} + : _colors.${color("md.comp.navigation-bar.inactive.label-text")} + ); + }); + } +} +'''; +} diff --git a/dev/tools/gen_defaults/lib/template.dart b/dev/tools/gen_defaults/lib/template.dart index 5c71c8d4fa5..6ed992c5124 100644 --- a/dev/tools/gen_defaults/lib/template.dart +++ b/dev/tools/gen_defaults/lib/template.dart @@ -76,17 +76,24 @@ abstract class TokenTemplate { /// Generate a shape constant for the given component token. /// - /// Currently only supports "SHAPE_FAMILY_ROUNDED_CORNERS" which it - /// maps to a [RoundedRectangleBorder] expression. + /// Currently supports family: + /// - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder]. + /// - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder]. String shape(String componentToken) { - // TODO(darrenaustin): handle more than just rounded rectangle shapes final Map shape = tokens[tokens['$componentToken.shape']!]! as Map; - return 'const RoundedRectangleBorder(borderRadius: ' - 'BorderRadius.only(' - 'topLeft: Radius.circular(${shape['topLeft']}), ' - 'topRight: Radius.circular(${shape['topRight']}), ' - 'bottomLeft: Radius.circular(${shape['bottomLeft']}), ' - 'bottomRight: Radius.circular(${shape['bottomRight']})))'; + switch (shape['family']) { + case 'SHAPE_FAMILY_ROUNDED_CORNERS': + return 'const RoundedRectangleBorder(borderRadius: ' + 'BorderRadius.only(' + 'topLeft: Radius.circular(${shape['topLeft']}), ' + 'topRight: Radius.circular(${shape['topRight']}), ' + 'bottomLeft: Radius.circular(${shape['bottomLeft']}), ' + 'bottomRight: Radius.circular(${shape['bottomRight']})))'; + case 'SHAPE_FAMILY_CIRCULAR': + return 'const StadiumBorder()'; + } + print('Unsupported shape family type: ${shape['family']} for $componentToken'); + return ''; } /// Generate a [TextTheme] text style name for the given component token. diff --git a/dev/tools/gen_defaults/test/gen_defaults_test.dart b/dev/tools/gen_defaults/test/gen_defaults_test.dart index 076c92d0ce9..34fe8648594 100644 --- a/dev/tools/gen_defaults/test/gen_defaults_test.dart +++ b/dev/tools/gen_defaults/test/gen_defaults_test.dart @@ -101,16 +101,21 @@ static final String tokenBar = 'bar'; test('Templates can get proper shapes from given data', () { const Map tokens = { 'foo.shape': 'shape.large', + 'bar.shape': 'shape.full', 'shape.large': { 'family': 'SHAPE_FAMILY_ROUNDED_CORNERS', 'topLeft': 1.0, 'topRight': 2.0, 'bottomLeft': 3.0, 'bottomRight': 4.0, - } + }, + 'shape.full': { + 'family': 'SHAPE_FAMILY_CIRCULAR', + }, }; final TestTemplate template = TestTemplate('foobar.dart', tokens); expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))'); + expect(template.shape('bar'), 'const StadiumBorder()'); }); } diff --git a/packages/flutter/lib/src/material/navigation_bar.dart b/packages/flutter/lib/src/material/navigation_bar.dart index 79d1691adb4..153f724b9be 100644 --- a/packages/flutter/lib/src/material/navigation_bar.dart +++ b/packages/flutter/lib/src/material/navigation_bar.dart @@ -71,6 +71,7 @@ class NavigationBar extends StatelessWidget { required this.destinations, this.onDestinationSelected, this.backgroundColor, + this.elevation, this.height, this.labelBehavior, }) : assert(destinations != null && destinations.length >= 2), @@ -111,6 +112,13 @@ class NavigationBar extends StatelessWidget { /// [ColorScheme.onSurface] using an [ElevationOverlay]. final Color? backgroundColor; + /// The elevation of the [NavigationBar] itself. + /// + /// If null, [NavigationBarThemeData.elevation] is used. If that + /// is also null, then if [ThemeData.useMaterial3] is true then it will + /// be 3.0 otherwise 0.0. + final double? elevation; + /// The height of the [NavigationBar] itself. /// /// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is @@ -145,20 +153,20 @@ class NavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; + final NavigationBarThemeData defaults = _defaultsFor(context); + final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); - final double effectiveHeight = height ?? navigationBarTheme.height ?? 80; + final double effectiveHeight = height ?? navigationBarTheme.height ?? defaults.height!; final NavigationDestinationLabelBehavior effectiveLabelBehavior = labelBehavior ?? navigationBarTheme.labelBehavior - ?? NavigationDestinationLabelBehavior.alwaysShow; + ?? defaults.labelBehavior!; final double additionalBottomPadding = MediaQuery.of(context).padding.bottom; return Material( - // With Material 3, the NavigationBar uses an overlay blend for the - // default color regardless of light/dark mode. color: backgroundColor - ?? navigationBarTheme.backgroundColor - ?? ElevationOverlay.colorWithOverlay(colorScheme.surface, colorScheme.onSurface, 3.0), + ?? navigationBarTheme.backgroundColor + ?? defaults.backgroundColor!, + elevation: elevation ?? navigationBarTheme.elevation ?? defaults.elevation!, child: Padding( padding: EdgeInsets.only(bottom: additionalBottomPadding), child: MediaQuery.removePadding( @@ -275,25 +283,25 @@ class NavigationDestination extends StatelessWidget { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final ColorScheme colorScheme = theme.colorScheme; + const Set selectedState = {MaterialState.selected}; + const Set unselectedState = {}; + final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); + final NavigationBarThemeData defaults = _defaultsFor(context); final Animation animation = _NavigationDestinationInfo.of(context).selectedAnimation; return _NavigationDestinationBuilder( label: label, tooltip: tooltip, buildIcon: (BuildContext context) { - final IconThemeData defaultIconTheme = IconThemeData( - size: 24, - color: colorScheme.onSurface, - ); final Widget selectedIconWidget = IconTheme.merge( - data: navigationBarTheme.iconTheme?.resolve({MaterialState.selected}) ?? defaultIconTheme, + data: navigationBarTheme.iconTheme?.resolve(selectedState) + ?? defaults.iconTheme!.resolve(selectedState)!, child: selectedIcon ?? icon, ); final Widget unselectedIconWidget = IconTheme.merge( - data: navigationBarTheme.iconTheme?.resolve({}) ?? defaultIconTheme, + data: navigationBarTheme.iconTheme?.resolve(unselectedState) + ?? defaults.iconTheme!.resolve(unselectedState)!, child: icon, ); @@ -302,25 +310,25 @@ class NavigationDestination extends StatelessWidget { children: [ NavigationIndicator( animation: animation, - color: navigationBarTheme.indicatorColor, + color: navigationBarTheme.indicatorColor ?? defaults.indicatorColor!, + shape: navigationBarTheme.indicatorShape ?? defaults.indicatorShape! ), _StatusTransitionWidgetBuilder( animation: animation, builder: (BuildContext context, Widget? child) { return _isForwardOrCompleted(animation) - ? selectedIconWidget - : unselectedIconWidget; + ? selectedIconWidget + : unselectedIconWidget; }, ), ], ); }, buildLabel: (BuildContext context) { - final TextStyle? defaultTextStyle = theme.textTheme.overline?.copyWith( - color: colorScheme.onSurface, - ); - final TextStyle? effectiveSelectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve({MaterialState.selected}) ?? defaultTextStyle; - final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve({}) ?? defaultTextStyle; + final TextStyle? effectiveSelectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(selectedState) + ?? defaults.labelTextStyle!.resolve(selectedState); + final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(unselectedState) + ?? defaults.labelTextStyle!.resolve(unselectedState); return Padding( padding: const EdgeInsets.only(top: 4), child: _ClampTextScaleFactor( @@ -525,6 +533,7 @@ class NavigationIndicator extends StatelessWidget { this.width = 64, this.height = 32, this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.shape, }) : super(key: key); /// Determines the scale of the indicator. @@ -538,25 +547,33 @@ class NavigationIndicator extends StatelessWidget { /// If null, defaults to [ColorScheme.secondary]. final Color? color; - /// The width of the container that holds in the indicator. + /// The width of this indicator. /// /// Defaults to `64`. final double width; - /// The height of the container that holds in the indicator. + /// The height of this indicator. /// /// Defaults to `32`. final double height; - /// The radius of the container that holds in the indicator. + /// The border radius of the shape of the indicator. + /// + /// This is used to create a [RoundedRectangleBorder] shape for the indicator. + /// This is ignored if [shape] is non-null. /// /// Defaults to `BorderRadius.circular(16)`. final BorderRadius borderRadius; + /// The shape of the indicator. + /// + /// If non-null this is used as the shape used to draw the background + /// of the indicator. If null then a [RoundedRectangleBorder] with the + /// [borderRadius] is used. + final ShapeBorder? shape; + @override Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { @@ -594,9 +611,9 @@ class NavigationIndicator extends StatelessWidget { child: Container( width: width, height: height, - decoration: BoxDecoration( - borderRadius: borderRadius, - color: color ?? colorScheme.secondary.withOpacity(.24), + decoration: ShapeDecoration( + shape: shape ?? RoundedRectangleBorder(borderRadius: borderRadius), + color: color ?? Theme.of(context).colorScheme.secondary, ), ), ); @@ -1171,3 +1188,90 @@ bool _isForwardOrCompleted(Animation animation) { return animation.status == AnimationStatus.forward || animation.status == AnimationStatus.completed; } + +NavigationBarThemeData _defaultsFor(BuildContext context) { + return Theme.of(context).useMaterial3 ? _TokenDefaultsM3(context) : _Defaults(context); +} + +class _Defaults extends NavigationBarThemeData { + _Defaults(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + height: 80.0, + elevation: 0.0, + indicatorShape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + // With Material 3, the NavigationBar uses an overlay blend for the + // default color regardless of light/dark mode. + @override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.surface, _colors.onSurface, 3.0); + + @override MaterialStateProperty? get iconTheme { + return MaterialStateProperty.all(IconThemeData( + size: 24, + color: _colors.onSurface, + )); + } + + @override Color? get indicatorColor => _colors.secondary.withOpacity(0.24); + + @override MaterialStateProperty? get labelTextStyle => MaterialStateProperty.all(_theme.textTheme.overline!.copyWith(color: _colors.onSurface)); +} + +// BEGIN GENERATED TOKEN PROPERTIES + +// Generated code to the end of this file. Do not edit by hand. +// These defaults are generated from the Material Design Token +// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Generated version v0_81 +class _TokenDefaultsM3 extends NavigationBarThemeData { + _TokenDefaultsM3(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + height: 80.0, + elevation: 3.0, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + // With Material 3, the NavigationBar uses an overlay blend for the + // default color regardless of light/dark mode. This should be handled + // in the Material widget based off of elevation, but for now we will do + // it here in the defaults. + @override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.surface, _colors.primary, 3.0); + + @override MaterialStateProperty? get iconTheme { + return MaterialStateProperty.resolveWith((Set states) { + return IconThemeData( + size: 24.0, + color: states.contains(MaterialState.selected) + ? _colors.onSecondaryContainer + : _colors.onSurfaceVariant, + ); + }); + } + + @override Color? get indicatorColor => _colors.secondaryContainer; + @override ShapeBorder? get indicatorShape => const StadiumBorder(); + + @override MaterialStateProperty? get labelTextStyle { + return MaterialStateProperty.resolveWith((Set states) { + final TextStyle style = _theme.textTheme.labelMedium!; + return style.apply(color: states.contains(MaterialState.selected) + ? _colors.onSurface + : _colors.onSurfaceVariant + ); + }); + } +} + +// END GENERATED TOKEN PROPERTIES diff --git a/packages/flutter/lib/src/material/navigation_bar_theme.dart b/packages/flutter/lib/src/material/navigation_bar_theme.dart index f3f0223cb51..93ee6202a54 100644 --- a/packages/flutter/lib/src/material/navigation_bar_theme.dart +++ b/packages/flutter/lib/src/material/navigation_bar_theme.dart @@ -41,7 +41,9 @@ class NavigationBarThemeData with Diagnosticable { const NavigationBarThemeData({ this.height, this.backgroundColor, + this.elevation, this.indicatorColor, + this.indicatorShape, this.labelTextStyle, this.iconTheme, this.labelBehavior, @@ -53,9 +55,15 @@ class NavigationBarThemeData with Diagnosticable { /// Overrides the default value of [NavigationBar.backgroundColor]. final Color? backgroundColor; + /// Overrides the default value of [NavigationBar.elevation]. + final double? elevation; + /// Overrides the default value of [NavigationBar]'s selection indicator. final Color? indicatorColor; + /// Overrides the default shape of the [NavigationBar]'s selection indicator. + final ShapeBorder? indicatorShape; + /// The style to merge with the default text style for /// [NavigationDestination] labels. /// @@ -77,7 +85,9 @@ class NavigationBarThemeData with Diagnosticable { NavigationBarThemeData copyWith({ double? height, Color? backgroundColor, + double? elevation, Color? indicatorColor, + ShapeBorder? indicatorShape, MaterialStateProperty? labelTextStyle, MaterialStateProperty? iconTheme, NavigationDestinationLabelBehavior? labelBehavior, @@ -85,7 +95,9 @@ class NavigationBarThemeData with Diagnosticable { return NavigationBarThemeData( height: height ?? this.height, backgroundColor: backgroundColor ?? this.backgroundColor, + elevation: elevation ?? this.elevation, indicatorColor: indicatorColor ?? this.indicatorColor, + indicatorShape: indicatorShape ?? this.indicatorShape, labelTextStyle: labelTextStyle ?? this.labelTextStyle, iconTheme: iconTheme ?? this.iconTheme, labelBehavior: labelBehavior ?? this.labelBehavior, @@ -104,7 +116,9 @@ class NavigationBarThemeData with Diagnosticable { return NavigationBarThemeData( height: lerpDouble(a?.height, b?.height, t), backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t), + indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t), labelTextStyle: _lerpProperties(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp), iconTheme: _lerpProperties(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp), labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior, @@ -116,7 +130,9 @@ class NavigationBarThemeData with Diagnosticable { return hashValues( height, backgroundColor, + elevation, indicatorColor, + indicatorShape, labelTextStyle, iconTheme, labelBehavior, @@ -132,7 +148,9 @@ class NavigationBarThemeData with Diagnosticable { return other is NavigationBarThemeData && other.height == height && other.backgroundColor == backgroundColor + && other.elevation == elevation && other.indicatorColor == indicatorColor + && other.indicatorShape == indicatorShape && other.labelTextStyle == labelTextStyle && other.iconTheme == iconTheme && other.labelBehavior == labelBehavior; @@ -143,7 +161,9 @@ class NavigationBarThemeData with Diagnosticable { super.debugFillProperties(properties); properties.add(DoubleProperty('height', height, defaultValue: null)); properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); + properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: null)); + properties.add(DiagnosticsProperty('indicatorShape', indicatorShape, defaultValue: null)); properties.add(DiagnosticsProperty>('labelTextStyle', labelTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty>('iconTheme', iconTheme, defaultValue: null)); properties.add(DiagnosticsProperty('labelBehavior', labelBehavior, defaultValue: null)); diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index c742b01123e..cf288a15202 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1150,6 +1150,7 @@ class ThemeData with Diagnosticable { /// Components that have been migrated to Material 3 are: /// /// * [FloatingActionButton] + /// * [NavigationBar] /// /// See also: /// diff --git a/packages/flutter/test/material/navigation_bar_test.dart b/packages/flutter/test/material/navigation_bar_test.dart index 615b14d8c20..d8fb87c57f0 100644 --- a/packages/flutter/test/material/navigation_bar_test.dart +++ b/packages/flutter/test/material/navigation_bar_test.dart @@ -63,6 +63,31 @@ void main() { expect(_getMaterial(tester).color, equals(color)); }); + testWidgets('NavigationBar can update elevation', (WidgetTester tester) async { + const double elevation = 42.0; + + await tester.pumpWidget( + _buildWidget( + NavigationBar( + elevation: elevation, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) {}, + ), + ), + ); + + expect(_getMaterial(tester).elevation, equals(elevation)); + }); + testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async { const double bottomPadding = 40.0; @@ -112,6 +137,61 @@ void main() { expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight); }); + testWidgets('NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async { + // Pre-M3 settings that were hand coded. + await tester.pumpWidget( + _buildWidget( + NavigationBar( + destinations: const [ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) {}, + ), + ), + ); + + expect(_getMaterial(tester).color, const Color(0xffeaeaea)); + expect(_getMaterial(tester).elevation, 0); + expect(tester.getSize(find.byType(NavigationBar)).height, 80); + expect(_indicator(tester)?.color, const Color(0x3d2196f3)); + expect(_indicator(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))); + + // M3 settings from the token database. + await tester.pumpWidget( + _buildWidget( + Theme( + data: ThemeData.light().copyWith(useMaterial3: true), + child: NavigationBar( + destinations: const [ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) {}, + ), + ), + ), + ); + + expect(_getMaterial(tester).color, const Color(0xffecf6fe)); + expect(_getMaterial(tester).elevation, 3); + expect(tester.getSize(find.byType(NavigationBar)).height, 80); + expect(_indicator(tester)?.color, const Color(0xff2196f3)); + expect(_indicator(tester)?.shape, const StadiumBorder()); + }); + testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async { const String label = 'A'; @@ -390,3 +470,12 @@ Material _getMaterial(WidgetTester tester) { find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)), ); } + +ShapeDecoration? _indicator(WidgetTester tester) { + return tester.firstWidget( + find.descendant( + of: find.byType(FadeTransition), + matching: find.byType(Container), + ), + ).decoration as ShapeDecoration?; +} diff --git a/packages/flutter/test/material/navigation_bar_theme_test.dart b/packages/flutter/test/material/navigation_bar_theme_test.dart index 684336ab392..6756577f51e 100644 --- a/packages/flutter/test/material/navigation_bar_theme_test.dart +++ b/packages/flutter/test/material/navigation_bar_theme_test.dart @@ -29,7 +29,9 @@ void main() { NavigationBarThemeData( height: 200.0, backgroundColor: const Color(0x00000099), + elevation: 20.0, indicatorColor: const Color(0x00000098), + indicatorShape: const CircleBorder(), labelTextStyle: MaterialStateProperty.all(const TextStyle(fontSize: 7.0)), iconTheme: MaterialStateProperty.all(const IconThemeData(color: Color(0x00000097))), labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, @@ -42,20 +44,24 @@ void main() { expect(description[0], 'height: 200.0'); expect(description[1], 'backgroundColor: Color(0x00000099)'); - expect(description[2], 'indicatorColor: Color(0x00000098)'); - expect(description[3], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))'); + expect(description[2], 'elevation: 20.0'); + expect(description[3], 'indicatorColor: Color(0x00000098)'); + expect(description[4], 'indicatorShape: CircleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))'); + expect(description[5], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))'); // Ignore instance address for IconThemeData. - expect(description[4].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue); - expect(description[4].contains('(color: Color(0x00000097))'), isTrue); + expect(description[6].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue); + expect(description[6].contains('(color: Color(0x00000097))'), isTrue); - expect(description[5], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide'); + expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide'); }); testWidgets('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async { const double height = 200.0; const Color backgroundColor = Color(0x00000001); + const double elevation = 42.0; const Color indicatorColor = Color(0x00000002); + const ShapeBorder indicatorShape = CircleBorder(); const double selectedIconSize = 25.0; const double unselectedIconSize = 23.0; const Color selectedIconColor = Color(0x00000003); @@ -73,7 +79,9 @@ void main() { data: NavigationBarThemeData( height: height, backgroundColor: backgroundColor, + elevation: elevation, indicatorColor: indicatorColor, + indicatorShape: indicatorShape, iconTheme: MaterialStateProperty.resolveWith((Set states) { if (states.contains(MaterialState.selected)) { return const IconThemeData( @@ -106,7 +114,9 @@ void main() { expect(_barHeight(tester), height); expect(_barMaterial(tester).color, backgroundColor); + expect(_barMaterial(tester).elevation, elevation); expect(_indicator(tester)?.color, indicatorColor); + expect(_indicator(tester)?.shape, indicatorShape); expect(_selectedIconTheme(tester).size, selectedIconSize); expect(_selectedIconTheme(tester).color, selectedIconColor); expect(_selectedIconTheme(tester).opacity, selectedIconOpacity); @@ -121,6 +131,7 @@ void main() { testWidgets('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async { const double height = 200.0; const Color backgroundColor = Color(0x00000001); + const double elevation = 42.0; const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow; await tester.pumpWidget( @@ -129,11 +140,13 @@ void main() { bottomNavigationBar: NavigationBarTheme( data: const NavigationBarThemeData( height: 100.0, + elevation: 18.0, backgroundColor: Color(0x00000099), labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, ), child: NavigationBar( height: height, + elevation: elevation, backgroundColor: backgroundColor, labelBehavior: labelBehavior, destinations: _destinations(), @@ -145,6 +158,7 @@ void main() { expect(_barHeight(tester), height); expect(_barMaterial(tester).color, backgroundColor); + expect(_barMaterial(tester).elevation, elevation); expect(_labelBehavior(tester), labelBehavior); }); } @@ -179,13 +193,13 @@ Material _barMaterial(WidgetTester tester) { ); } -BoxDecoration? _indicator(WidgetTester tester) { +ShapeDecoration? _indicator(WidgetTester tester) { return tester.firstWidget( find.descendant( of: find.byType(FadeTransition), matching: find.byType(Container), ), - ).decoration as BoxDecoration?; + ).decoration as ShapeDecoration?; } IconThemeData _selectedIconTheme(WidgetTester tester) { diff --git a/packages/flutter/test/material/navigation_rail_theme_test.dart b/packages/flutter/test/material/navigation_rail_theme_test.dart index 05243ba917f..f9dc4772079 100644 --- a/packages/flutter/test/material/navigation_rail_theme_test.dart +++ b/packages/flutter/test/material/navigation_rail_theme_test.dart @@ -263,13 +263,13 @@ Material _railMaterial(WidgetTester tester) { } -BoxDecoration? _indicatorDecoration(WidgetTester tester) { +ShapeDecoration? _indicatorDecoration(WidgetTester tester) { return tester.firstWidget( find.descendant( of: find.byType(NavigationIndicator), matching: find.byType(Container), ), - ).decoration as BoxDecoration?; + ).decoration as ShapeDecoration?; } IconThemeData _selectedIconTheme(WidgetTester tester) {