From cb988c7b6e60bab6ea6480cc3b51e07b797dfdc7 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 19 Dec 2022 20:39:27 +0200 Subject: [PATCH] Add `indicatorColor` & `indicatorShape` to `NavigationRail`, `NavigationDrawer` and move these properties from destination to `NavigationBar` (#117049) --- .../lib/src/material/navigation_bar.dart | 45 +++++++++---- .../lib/src/material/navigation_drawer.dart | 34 +++++++++- .../lib/src/material/navigation_rail.dart | 21 ++++++- .../test/material/navigation_bar_test.dart | 62 +++++++++--------- .../test/material/navigation_drawer_test.dart | 63 ++++++++++++++++--- .../test/material/navigation_rail_test.dart | 54 +++++++++++++++- 6 files changed, 224 insertions(+), 55 deletions(-) diff --git a/packages/flutter/lib/src/material/navigation_bar.dart b/packages/flutter/lib/src/material/navigation_bar.dart index b17704bdfd4b..103f2ff996df 100644 --- a/packages/flutter/lib/src/material/navigation_bar.dart +++ b/packages/flutter/lib/src/material/navigation_bar.dart @@ -93,6 +93,8 @@ class NavigationBar extends StatelessWidget { this.elevation, this.shadowColor, this.surfaceTintColor, + this.indicatorColor, + this.indicatorShape, this.height, this.labelBehavior, }) : assert(destinations != null && destinations.length >= 2), @@ -158,6 +160,20 @@ class NavigationBar extends StatelessWidget { /// overlay is applied. final Color? surfaceTintColor; + /// The color of the [indicatorShape] when this destination is selected. + /// + /// If null, [NavigationBarThemeData.indicatorColor] is used. If that + /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.secondaryContainer] + /// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used. + final Color? indicatorColor; + + /// The shape of the selected inidicator. + /// + /// If null, [NavigationBarThemeData.indicatorShape] is used. If that + /// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used. + /// Otherwise, [RoundedRectangleBorder] with a circular border radius of 16 is used. + final ShapeBorder? indicatorShape; + /// The height of the [NavigationBar] itself. /// /// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is @@ -224,6 +240,8 @@ class NavigationBar extends StatelessWidget { totalNumberOfDestinations: destinations.length, selectedAnimation: animation, labelBehavior: effectiveLabelBehavior, + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, onTap: _handleTap(i), child: destinations[i], ); @@ -274,8 +292,6 @@ class NavigationDestination extends StatelessWidget { super.key, required this.icon, this.selectedIcon, - this.indicatorColor, - this.indicatorShape, required this.label, this.tooltip, }); @@ -300,12 +316,6 @@ class NavigationDestination extends StatelessWidget { /// would use a size of 24.0 and [ColorScheme.onSurface]. final Widget? selectedIcon; - /// The color of the [indicatorShape] when this destination is selected. - final Color? indicatorColor; - - /// The shape of the selected inidicator. - final ShapeBorder? indicatorShape; - /// The text label that appears below the icon of this /// [NavigationDestination]. /// @@ -324,12 +334,13 @@ class NavigationDestination extends StatelessWidget { @override Widget build(BuildContext context) { + final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context); 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; + final Animation animation = info.selectedAnimation; return _NavigationDestinationBuilder( label: label, @@ -351,8 +362,8 @@ class NavigationDestination extends StatelessWidget { children: [ NavigationIndicator( animation: animation, - color: indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!, - shape: indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape! + color: info.indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!, + shape: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape! ), _StatusTransitionWidgetBuilder( animation: animation, @@ -532,6 +543,8 @@ class _NavigationDestinationInfo extends InheritedWidget { required this.totalNumberOfDestinations, required this.selectedAnimation, required this.labelBehavior, + required this.indicatorColor, + required this.indicatorShape, required this.onTap, required super.child, }); @@ -588,6 +601,16 @@ class _NavigationDestinationInfo extends InheritedWidget { /// label, or hide all labels. final NavigationDestinationLabelBehavior labelBehavior; + /// The color of the selection indicator. + /// + /// This is used by destinations to override the indicator color. + final Color? indicatorColor; + + /// The shape of the selection indicator. + /// + /// This is used by destinations to override the indicator shape. + final ShapeBorder? indicatorShape; + /// The callback that should be called when this destination is tapped. /// /// This is computed by calling [NavigationBar.onDestinationSelected] diff --git a/packages/flutter/lib/src/material/navigation_drawer.dart b/packages/flutter/lib/src/material/navigation_drawer.dart index b31229314976..9ae97770d6cf 100644 --- a/packages/flutter/lib/src/material/navigation_drawer.dart +++ b/packages/flutter/lib/src/material/navigation_drawer.dart @@ -57,6 +57,8 @@ class NavigationDrawer extends StatelessWidget { this.shadowColor, this.surfaceTintColor, this.elevation, + this.indicatorColor, + this.indicatorShape, this.onDestinationSelected, this.selectedIndex = 0, }); @@ -90,6 +92,18 @@ class NavigationDrawer extends StatelessWidget { /// is also null, it will be 1.0. final double? elevation; + /// The color of the [indicatorShape] when this destination is selected. + /// + /// If this is null, [NavigationDrawerThemeData.indicatorColor] is used. + /// If that is also null, defaults to [ColorScheme.secondaryContainer]. + final Color? indicatorColor; + + /// The shape of the selected inidicator. + /// + /// If this is null, [NavigationDrawerThemeData.indicatorShape] is used. + /// If that is also null, defaults to [StadiumBorder]. + final ShapeBorder? indicatorShape; + /// Defines the appearance of the items within the navigation drawer. /// /// The list contains [NavigationDrawerDestination] widgets and/or customized @@ -125,6 +139,8 @@ class NavigationDrawer extends StatelessWidget { index: index, totalNumberOfDestinations: totalNumberOfDestinations, selectedAnimation: animation, + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, onTap: () { if (onDestinationSelected != null) { onDestinationSelected!(index); @@ -315,9 +331,9 @@ class _NavigationDestinationBuilder extends StatelessWidget { alignment: Alignment.center, children: [ NavigationIndicator( - animation: _NavigationDrawerDestinationInfo.of(context).selectedAnimation, - color: navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!, - shape: navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!, + animation: info.selectedAnimation, + color: info.indicatorColor ?? navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!, + shape: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!, width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width, height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height, ), @@ -433,6 +449,8 @@ class _NavigationDrawerDestinationInfo extends InheritedWidget { required this.index, required this.totalNumberOfDestinations, required this.selectedAnimation, + required this.indicatorColor, + required this.indicatorShape, required this.onTap, required super.child, }); @@ -478,6 +496,16 @@ class _NavigationDrawerDestinationInfo extends InheritedWidget { /// to 1 (selected). final Animation selectedAnimation; + /// The color of the indicator. + /// + /// This is used by destinations to override the indicator color. + final Color? indicatorColor; + + /// The shape of the indicator. + /// + /// This is used by destinations to override the indicator shape. + final ShapeBorder? indicatorShape; + /// The callback that should be called when this destination is tapped. /// /// This is computed by calling [NavigationDrawer.onDestinationSelected] diff --git a/packages/flutter/lib/src/material/navigation_rail.dart b/packages/flutter/lib/src/material/navigation_rail.dart index 688f1dbfaaf0..6beed46c555d 100644 --- a/packages/flutter/lib/src/material/navigation_rail.dart +++ b/packages/flutter/lib/src/material/navigation_rail.dart @@ -110,6 +110,7 @@ class NavigationRail extends StatefulWidget { this.minExtendedWidth, this.useIndicator, this.indicatorColor, + this.indicatorShape, }) : assert(destinations != null && destinations.length >= 2), assert(selectedIndex == null || (0 <= selectedIndex && selectedIndex < destinations.length)), assert(elevation == null || elevation > 0), @@ -306,8 +307,18 @@ class NavigationRail extends StatefulWidget { /// Overrides the default value of [NavigationRail]'s selection indicator color, /// when [useIndicator] is true. + /// + /// If this is null, [NavigationRailThemeData.indicatorColor] is used. If + /// that is null, defaults to [ColorScheme.secondaryContainer]. final Color? indicatorColor; + /// Overrides the default value of [NavigationRail]'s selection indicator shape, + /// when [useIndicator] is true. + /// + /// If this is null, [NavigationRailThemeData.indicatorShape] is used. If + /// that is null, defaults to [StadiumBorder]. + final ShapeBorder? indicatorShape; + /// Returns the animation that controls the [NavigationRail.extended] state. /// /// This can be used to synchronize animations in the [leading] or [trailing] @@ -396,7 +407,7 @@ class _NavigationRailState extends State with TickerProviderStat final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? defaults.labelType!; final bool useIndicator = widget.useIndicator ?? navigationRailTheme.useIndicator ?? defaults.useIndicator!; final Color? indicatorColor = widget.indicatorColor ?? navigationRailTheme.indicatorColor ?? defaults.indicatorColor; - final ShapeBorder? indicatorShape = navigationRailTheme.indicatorShape ?? defaults.indicatorShape; + final ShapeBorder? indicatorShape = widget.indicatorShape ?? navigationRailTheme.indicatorShape ?? defaults.indicatorShape; // For backwards compatibility, in M2 the opacity of the unselected icons needs // to be set to the default if it isn't in the given theme. This can be removed @@ -900,6 +911,8 @@ class NavigationRailDestination { const NavigationRailDestination({ required this.icon, Widget? selectedIcon, + this.indicatorColor, + this.indicatorShape, required this.label, this.padding, }) : selectedIcon = selectedIcon ?? icon, @@ -933,6 +946,12 @@ class NavigationRailDestination { /// icons. final Widget selectedIcon; + /// The color of the [indicatorShape] when this destination is selected. + final Color? indicatorColor; + + /// The shape of the selection inidicator. + final ShapeBorder? indicatorShape; + /// The label for the destination. /// /// The label must be provided when used with the [NavigationRail]. When the diff --git a/packages/flutter/test/material/navigation_bar_test.dart b/packages/flutter/test/material/navigation_bar_test.dart index cba5d376f312..edf1d23d2fcb 100644 --- a/packages/flutter/test/material/navigation_bar_test.dart +++ b/packages/flutter/test/material/navigation_bar_test.dart @@ -263,8 +263,8 @@ void main() { expect(_getMaterial(tester).surfaceTintColor, null); 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))); + expect(_getIndicatorDecoration(tester)?.color, const Color(0x3d2196f3)); + expect(_getIndicatorDecoration(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))); // M3 settings from the token database. await tester.pumpWidget( @@ -292,8 +292,8 @@ void main() { expect(_getMaterial(tester).surfaceTintColor, ThemeData().colorScheme.surfaceTint); 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()); + expect(_getIndicatorDecoration(tester)?.color, const Color(0xff2196f3)); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); }); testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async { @@ -807,21 +807,21 @@ void main() { testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const Color color = Color(0xff0000ff); - const ShapeBorder shape = CircleBorder(); + const ShapeBorder shape = RoundedRectangleBorder(); - Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) { + Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) { return MaterialApp( theme: theme, home: Scaffold( bottomNavigationBar: NavigationBar( - destinations: [ + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + destinations: const [ NavigationDestination( - icon: const Icon(Icons.ac_unit), + icon: Icon(Icons.ac_unit), label: 'AC', - indicatorColor: indicatorColor, - indicatorShape: indicatorShape, ), - const NavigationDestination( + NavigationDestination( icon: Icon(Icons.access_alarm), label: 'Alarm', ), @@ -832,17 +832,17 @@ void main() { ); } - await tester.pumpWidget(buildNaviagationBar()); + await tester.pumpWidget(buildNavigationBar()); // Test default indicator color and shape. - expect(_indicator(tester)?.color, theme.colorScheme.secondaryContainer); - expect(_indicator(tester)?.shape, const StadiumBorder()); + expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); - await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape)); + await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape)); // Test custom indicator color and shape. - expect(_indicator(tester)?.color, color); - expect(_indicator(tester)?.shape, shape); + expect(_getIndicatorDecoration(tester)?.color, color); + expect(_getIndicatorDecoration(tester)?.shape, shape); }); group('Material 2', () { @@ -852,21 +852,21 @@ void main() { testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); const Color color = Color(0xff0000ff); - const ShapeBorder shape = CircleBorder(); + const ShapeBorder shape = RoundedRectangleBorder(); - Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) { + Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) { return MaterialApp( theme: theme, home: Scaffold( bottomNavigationBar: NavigationBar( - destinations: [ + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + destinations: const [ NavigationDestination( - icon: const Icon(Icons.ac_unit), + icon: Icon(Icons.ac_unit), label: 'AC', - indicatorColor: indicatorColor, - indicatorShape: indicatorShape, ), - const NavigationDestination( + NavigationDestination( icon: Icon(Icons.access_alarm), label: 'Alarm', ), @@ -877,20 +877,20 @@ void main() { ); } - await tester.pumpWidget(buildNaviagationBar()); + await tester.pumpWidget(buildNavigationBar()); // Test default indicator color and shape. - expect(_indicator(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24)); + expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24)); expect( - _indicator(tester)?.shape, + _getIndicatorDecoration(tester)?.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), ); - await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape)); + await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape)); // Test custom indicator color and shape. - expect(_indicator(tester)?.color, color); - expect(_indicator(tester)?.shape, shape); + expect(_getIndicatorDecoration(tester)?.color, color); + expect(_getIndicatorDecoration(tester)?.shape, shape); }); }); } @@ -912,7 +912,7 @@ Material _getMaterial(WidgetTester tester) { ); } -ShapeDecoration? _indicator(WidgetTester tester) { +ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester.firstWidget( find.descendant( of: find.byType(FadeTransition), diff --git a/packages/flutter/test/material/navigation_drawer_test.dart b/packages/flutter/test/material/navigation_drawer_test.dart index 7816e8cc916e..b96e5faea7de 100644 --- a/packages/flutter/test/material/navigation_drawer_test.dart +++ b/packages/flutter/test/material/navigation_drawer_test.dart @@ -23,7 +23,7 @@ void main() { ), NavigationDrawerDestination( icon: Icon(Icons.access_alarm, color: theme.iconTheme.color), - label: Text('Alarm',style: theme.textTheme.bodySmall), + label: Text('Alarm', style: theme.textTheme.bodySmall), ), ], onDestinationSelected: (int i) { @@ -68,7 +68,7 @@ void main() { ), NavigationDrawerDestination( icon: Icon(Icons.access_alarm, color: theme.iconTheme.color), - label: Text('Alarm',style: theme.textTheme.bodySmall), + label: Text('Alarm', style: theme.textTheme.bodySmall), ), ], onDestinationSelected: (int i) {}, @@ -97,7 +97,7 @@ void main() { ), NavigationDrawerDestination( icon: Icon(Icons.access_alarm, color: theme.iconTheme.color), - label: Text('Alarm',style: theme.textTheme.bodySmall), + label: Text('Alarm', style: theme.textTheme.bodySmall), ), ], ); @@ -134,7 +134,7 @@ void main() { ), NavigationDrawerDestination( icon: Icon(Icons.access_alarm, color: theme.iconTheme.color), - label: Text('Alarm',style: theme.textTheme.bodySmall), + label: Text('Alarm', style: theme.textTheme.bodySmall), ), ], onDestinationSelected: (int i) {}, @@ -149,8 +149,8 @@ void main() { expect(_getMaterial(tester).surfaceTintColor, ThemeData().colorScheme.surfaceTint); expect(_getMaterial(tester).elevation, 1); - expect(_indicator(tester)?.color, const Color(0xff2196f3)); - expect(_indicator(tester)?.shape, const StadiumBorder()); + expect(_getIndicatorDecoration(tester)?.color, const Color(0xff2196f3)); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); }); testWidgets('Navigation drawer semantics', (WidgetTester tester) async { @@ -169,7 +169,7 @@ void main() { ), NavigationDrawerDestination( icon: Icon(Icons.access_alarm, color: theme.iconTheme.color), - label: Text('Alarm',style: theme.textTheme.bodySmall), + label: Text('Alarm', style: theme.textTheme.bodySmall), ), ], ), @@ -222,6 +222,53 @@ void main() { ), ); }); + + testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + final ThemeData theme = ThemeData(useMaterial3: true); + const Color color = Color(0xff0000ff); + const ShapeBorder shape = RoundedRectangleBorder(); + + Widget buildNavigationDrawer({Color? indicatorColor, ShapeBorder? indicatorShape}) { + return MaterialApp( + theme: theme, + home: Scaffold( + key: scaffoldKey, + drawer: NavigationDrawer( + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + children: [ + Text('Headline', style: theme.textTheme.bodyLarge), + const NavigationDrawerDestination( + icon: Icon(Icons.ac_unit), + label: Text('AC'), + ), + const NavigationDrawerDestination( + icon: Icon(Icons.access_alarm), + label: Text('Alarm'), + ), + ], + onDestinationSelected: (int i) { }, + ), + body: Container(), + ), + ); + } + + await tester.pumpWidget(buildNavigationDrawer()); + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + // Test default indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); + + await tester.pumpWidget(buildNavigationDrawer(indicatorColor: color, indicatorShape: shape)); + + // Test custom indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, color); + expect(_getIndicatorDecoration(tester)?.shape, shape); + }); } Widget _buildWidget(GlobalKey scaffoldKey, Widget child) { @@ -242,7 +289,7 @@ Material _getMaterial(WidgetTester tester) { ); } -ShapeDecoration? _indicator(WidgetTester tester) { +ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( find.descendant( diff --git a/packages/flutter/test/material/navigation_rail_test.dart b/packages/flutter/test/material/navigation_rail_test.dart index f23539e68e82..cd040e7fda07 100644 --- a/packages/flutter/test/material/navigation_rail_test.dart +++ b/packages/flutter/test/material/navigation_rail_test.dart @@ -2846,6 +2846,50 @@ void main() { expect(transform.getColumn(0)[0], 1.0); }); + testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + const Color color = Color(0xff0000ff); + const ShapeBorder shape = RoundedRectangleBorder(); + + Widget buildNavigationRail({Color? indicatorColor, ShapeBorder? indicatorShape}) { + return MaterialApp( + theme: theme, + home: Builder( + builder: (BuildContext context) { + return Scaffold( + body: Row( + children: [ + NavigationRail( + useIndicator: true, + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + selectedIndex: 0, + destinations: _destinations(), + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ); + }, + ), + ); + } + + await tester.pumpWidget(buildNavigationRail()); + + // Test default indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); + + await tester.pumpWidget(buildNavigationRail(indicatorColor: color, indicatorShape: shape)); + + // Test custom indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, color); + expect(_getIndicatorDecoration(tester)?.shape, shape); + }); + group('Material 2', () { // Original Material 2 tests. Remove this group after `useMaterial3` has been deprecated. testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { @@ -4655,7 +4699,6 @@ void main() { final double updatedWidthRTL = tester.getSize(find.byType(NavigationRail)).width; expect(updatedWidthRTL, defaultWidth + safeAreaPadding); }); - }); // End Material 2 group } @@ -4878,3 +4921,12 @@ Widget _buildWidget(Widget child, {bool useMaterial3 = true, bool isRTL = false} ), ); } + +ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { + return tester.firstWidget( + find.descendant( + of: find.byType(FadeTransition), + matching: find.byType(Container), + ), + ).decoration as ShapeDecoration?; +}