Skip to content

Commit 6d998fe

Browse files
authored
[CP] Add ability to customize NavigationBar indicator overlay and fix in… (flutter#139162)
�dicator shape for the overlay (flutter#138901) fixes [Provide ability to override `NavigationBar` indicator ink response overlay](flutter#138850) fixes [`NavigationBar.indicatorShape` is ignored, `NavigationBarThemeData.indicatorShape` is applied to the indicator inkwell](flutter#138900) --- Cherry pick fixes flutter#139159 --- ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @OverRide Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( bottomNavigationBar: NavigationBarExample(), ), ); } } class NavigationBarExample extends StatefulWidget { const NavigationBarExample({super.key}); @OverRide State<NavigationBarExample> createState() => _NavigationBarExampleState(); } class _NavigationBarExampleState extends State<NavigationBarExample> { int index = 0; @OverRide Widget build(BuildContext context) { return NavigationBar( elevation: 0, overlayColor: const MaterialStatePropertyAll<Color>(Colors.transparent), // indicatorShape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(4.0), // ), indicatorColor: Colors.transparent, selectedIndex: index, onDestinationSelected: (int index) { setState(() { this.index = index; }); }, destinations: const <Widget>[ NavigationDestination( selectedIcon: Icon(Icons.home_filled), icon: Icon(Icons.home_outlined), label: 'Home', ), NavigationDestination( selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_outline), label: 'Favorites', ), ], ); } } ``` </details> ### Before #### Cannot override `NavigationBar` Indicator ink well overlay ![Screenshot 2023-11-22 at 18 22 48](https://github.com/flutter/flutter/assets/48603081/06f54335-71ee-4882-afb0-53b614933c38) #### Indicator shape is ignored for the indicator overlay ![Screenshot 2023-11-22 at 15 29 52](https://github.com/flutter/flutter/assets/48603081/913e0f77-48f4-4c6e-87f3-52c81b78f3d9) ### After #### Can use `NavigationBar.overlayColor` or `NavigationBarThemeData.NavigationBar` to override default indicator overlay `overlayColor: MaterialStatePropertyAll<Color>(Colors.red.withOpacity(0.33)),` ![Screenshot 2023-11-22 at 18 22 08](https://github.com/flutter/flutter/assets/48603081/28badae4-a7c7-4bf0-8bcc-278a1f84729d) `overlayColor: MaterialStatePropertyAll<Color>(Colors.transparent),` ![Screenshot 2023-11-22 at 18 22 25](https://github.com/flutter/flutter/assets/48603081/674b48b1-f66a-4d91-9f10-ad307416ac32) #### Indicator shape is respected for the indicator overlay ![Screenshot 2023-11-22 at 15 30 36](https://github.com/flutter/flutter/assets/48603081/ae9a3627-787e-45ac-9319-2ea8ea1e6ae6) *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
1 parent 784a44f commit 6d998fe

File tree

4 files changed

+229
-28
lines changed

4 files changed

+229
-28
lines changed

packages/flutter/lib/src/material/navigation_bar.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class NavigationBar extends StatelessWidget {
9898
this.indicatorShape,
9999
this.height,
100100
this.labelBehavior,
101+
this.overlayColor,
101102
}) : assert(destinations.length >= 2),
102103
assert(0 <= selectedIndex && selectedIndex < destinations.length);
103104

@@ -201,6 +202,10 @@ class NavigationBar extends StatelessWidget {
201202
/// [NavigationDestinationLabelBehavior.alwaysShow].
202203
final NavigationDestinationLabelBehavior? labelBehavior;
203204

205+
/// The highlight color that's typically used to indicate that
206+
/// the [NavigationDestination] is focused, hovered, or pressed.
207+
final MaterialStateProperty<Color?>? overlayColor;
208+
204209
VoidCallback _handleTap(int index) {
205210
return onDestinationSelected != null
206211
? () => onDestinationSelected!(index)
@@ -243,6 +248,7 @@ class NavigationBar extends StatelessWidget {
243248
labelBehavior: effectiveLabelBehavior,
244249
indicatorColor: indicatorColor,
245250
indicatorShape: indicatorShape,
251+
overlayColor: overlayColor,
246252
onTap: _handleTap(i),
247253
child: destinations[i],
248254
);
@@ -503,7 +509,8 @@ class _NavigationDestinationBuilderState extends State<_NavigationDestinationBui
503509
child: _IndicatorInkWell(
504510
iconKey: iconKey,
505511
labelBehavior: info.labelBehavior,
506-
customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
512+
customBorder: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
513+
overlayColor: info.overlayColor ?? navigationBarTheme.overlayColor,
507514
onTap: widget.enabled ? info.onTap : null,
508515
child: Row(
509516
children: <Widget>[
@@ -526,6 +533,7 @@ class _IndicatorInkWell extends InkResponse {
526533
const _IndicatorInkWell({
527534
required this.iconKey,
528535
required this.labelBehavior,
536+
super.overlayColor,
529537
super.customBorder,
530538
super.onTap,
531539
super.child,
@@ -563,6 +571,7 @@ class _NavigationDestinationInfo extends InheritedWidget {
563571
required this.labelBehavior,
564572
required this.indicatorColor,
565573
required this.indicatorShape,
574+
required this.overlayColor,
566575
required this.onTap,
567576
required super.child,
568577
});
@@ -629,6 +638,12 @@ class _NavigationDestinationInfo extends InheritedWidget {
629638
/// This is used by destinations to override the indicator shape.
630639
final ShapeBorder? indicatorShape;
631640

641+
/// The highlight color that's typically used to indicate that
642+
/// the [NavigationDestination] is focused, hovered, or pressed.
643+
///
644+
/// This is used by destinations to override the overlay color.
645+
final MaterialStateProperty<Color?>? overlayColor;
646+
632647
/// The callback that should be called when this destination is tapped.
633648
///
634649
/// This is computed by calling [NavigationBar.onDestinationSelected]

packages/flutter/lib/src/material/navigation_bar_theme.dart

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class NavigationBarThemeData with Diagnosticable {
5252
this.labelTextStyle,
5353
this.iconTheme,
5454
this.labelBehavior,
55+
this.overlayColor,
5556
});
5657

5758
/// Overrides the default value of [NavigationBar.height].
@@ -91,6 +92,9 @@ class NavigationBarThemeData with Diagnosticable {
9192
/// Overrides the default value of [NavigationBar.labelBehavior].
9293
final NavigationDestinationLabelBehavior? labelBehavior;
9394

95+
/// Overrides the default value of [NavigationBar.overlayColor].
96+
final MaterialStateProperty<Color?>? overlayColor;
97+
9498
/// Creates a copy of this object with the given fields replaced with the
9599
/// new values.
96100
NavigationBarThemeData copyWith({
@@ -104,6 +108,7 @@ class NavigationBarThemeData with Diagnosticable {
104108
MaterialStateProperty<TextStyle?>? labelTextStyle,
105109
MaterialStateProperty<IconThemeData?>? iconTheme,
106110
NavigationDestinationLabelBehavior? labelBehavior,
111+
MaterialStateProperty<Color?>? overlayColor,
107112
}) {
108113
return NavigationBarThemeData(
109114
height: height ?? this.height,
@@ -116,6 +121,7 @@ class NavigationBarThemeData with Diagnosticable {
116121
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
117122
iconTheme: iconTheme ?? this.iconTheme,
118123
labelBehavior: labelBehavior ?? this.labelBehavior,
124+
overlayColor: overlayColor ?? this.overlayColor,
119125
);
120126
}
121127

@@ -139,6 +145,7 @@ class NavigationBarThemeData with Diagnosticable {
139145
labelTextStyle: MaterialStateProperty.lerp<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
140146
iconTheme: MaterialStateProperty.lerp<IconThemeData?>(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
141147
labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior,
148+
overlayColor: MaterialStateProperty.lerp<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
142149
);
143150
}
144151

@@ -154,6 +161,7 @@ class NavigationBarThemeData with Diagnosticable {
154161
labelTextStyle,
155162
iconTheme,
156163
labelBehavior,
164+
overlayColor,
157165
);
158166

159167
@override
@@ -165,16 +173,17 @@ class NavigationBarThemeData with Diagnosticable {
165173
return false;
166174
}
167175
return other is NavigationBarThemeData
168-
&& other.height == height
169-
&& other.backgroundColor == backgroundColor
170-
&& other.elevation == elevation
171-
&& other.shadowColor == shadowColor
172-
&& other.surfaceTintColor == surfaceTintColor
173-
&& other.indicatorColor == indicatorColor
174-
&& other.indicatorShape == indicatorShape
175-
&& other.labelTextStyle == labelTextStyle
176-
&& other.iconTheme == iconTheme
177-
&& other.labelBehavior == labelBehavior;
176+
&& other.height == height
177+
&& other.backgroundColor == backgroundColor
178+
&& other.elevation == elevation
179+
&& other.shadowColor == shadowColor
180+
&& other.surfaceTintColor == surfaceTintColor
181+
&& other.indicatorColor == indicatorColor
182+
&& other.indicatorShape == indicatorShape
183+
&& other.labelTextStyle == labelTextStyle
184+
&& other.iconTheme == iconTheme
185+
&& other.labelBehavior == labelBehavior
186+
&& other.overlayColor == overlayColor;
178187
}
179188

180189
@override
@@ -190,6 +199,7 @@ class NavigationBarThemeData with Diagnosticable {
190199
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
191200
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>('iconTheme', iconTheme, defaultValue: null));
192201
properties.add(DiagnosticsProperty<NavigationDestinationLabelBehavior>('labelBehavior', labelBehavior, defaultValue: null));
202+
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
193203
}
194204
}
195205

packages/flutter/test/material/navigation_bar_test.dart

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'dart:math';
1212
import 'package:flutter/foundation.dart';
1313
import 'package:flutter/gestures.dart';
1414
import 'package:flutter/material.dart';
15+
import 'package:flutter/services.dart';
1516
import 'package:flutter_test/flutter_test.dart';
1617
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
1718

@@ -937,28 +938,30 @@ void main() {
937938
});
938939

939940
testWidgetsWithLeakTracking('Material3 - Navigation destination updates indicator color and shape', (WidgetTester tester) async {
940-
final ThemeData theme = ThemeData(useMaterial3: true);
941+
final ThemeData theme = ThemeData();
941942
const Color color = Color(0xff0000ff);
942943
const ShapeBorder shape = RoundedRectangleBorder();
943944

944945
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
945946
return MaterialApp(
946947
theme: theme,
947948
home: Scaffold(
948-
bottomNavigationBar: NavigationBar(
949-
indicatorColor: indicatorColor,
950-
indicatorShape: indicatorShape,
951-
destinations: const <Widget>[
952-
NavigationDestination(
953-
icon: Icon(Icons.ac_unit),
954-
label: 'AC',
955-
),
956-
NavigationDestination(
957-
icon: Icon(Icons.access_alarm),
958-
label: 'Alarm',
959-
),
960-
],
961-
onDestinationSelected: (int i) { },
949+
bottomNavigationBar: RepaintBoundary(
950+
child: NavigationBar(
951+
indicatorColor: indicatorColor,
952+
indicatorShape: indicatorShape,
953+
destinations: const <Widget>[
954+
NavigationDestination(
955+
icon: Icon(Icons.ac_unit),
956+
label: 'AC',
957+
),
958+
NavigationDestination(
959+
icon: Icon(Icons.access_alarm),
960+
label: 'Alarm',
961+
),
962+
],
963+
onDestinationSelected: (int i) { },
964+
),
962965
),
963966
),
964967
);
@@ -970,11 +973,22 @@ void main() {
970973
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
971974
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
972975

976+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
977+
await gesture.addPointer();
978+
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
979+
await tester.pumpAndSettle();
980+
981+
// Test default indicator color and shape with ripple.
982+
await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.default.indicator.inkwell.shape.png'));
983+
973984
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
974985

975986
// Test custom indicator color and shape.
976987
expect(_getIndicatorDecoration(tester)?.color, color);
977988
expect(_getIndicatorDecoration(tester)?.shape, shape);
989+
990+
// Test custom indicator color and shape with ripple.
991+
await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.custom.indicator.inkwell.shape.png'));
978992
});
979993

980994
testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async {
@@ -1014,6 +1028,86 @@ void main() {
10141028
expect(selectedIndex, 1);
10151029
});
10161030

1031+
testWidgetsWithLeakTracking('NavigationBar respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
1032+
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
1033+
const Color hoverColor = Color(0xff0000ff);
1034+
const Color focusColor = Color(0xff00ffff);
1035+
const Color pressedColor = Color(0xffff00ff);
1036+
final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>(
1037+
(Set<MaterialState> states) {
1038+
if (states.contains(MaterialState.hovered)) {
1039+
return hoverColor;
1040+
}
1041+
if (states.contains(MaterialState.focused)) {
1042+
return focusColor;
1043+
}
1044+
if (states.contains(MaterialState.pressed)) {
1045+
return pressedColor;
1046+
}
1047+
return Colors.transparent;
1048+
});
1049+
1050+
await tester.pumpWidget(MaterialApp(
1051+
home: Scaffold(
1052+
bottomNavigationBar: RepaintBoundary(
1053+
child: NavigationBar(
1054+
overlayColor: overlayColor,
1055+
destinations: const <Widget>[
1056+
NavigationDestination(
1057+
icon: Icon(Icons.ac_unit),
1058+
label: 'AC',
1059+
),
1060+
NavigationDestination(
1061+
icon: Icon(Icons.access_alarm),
1062+
label: 'Alarm',
1063+
),
1064+
],
1065+
onDestinationSelected: (int i) { },
1066+
),
1067+
),
1068+
),
1069+
));
1070+
1071+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1072+
await gesture.addPointer();
1073+
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
1074+
await tester.pumpAndSettle();
1075+
1076+
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
1077+
1078+
// Test hovered state.
1079+
expect(
1080+
inkFeatures,
1081+
kIsWeb
1082+
? (paints..rrect()..rrect()..circle(color: hoverColor))
1083+
: (paints..circle(color: hoverColor)),
1084+
);
1085+
1086+
await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
1087+
await tester.pumpAndSettle();
1088+
1089+
// Test pressed state.
1090+
expect(
1091+
inkFeatures,
1092+
kIsWeb
1093+
? (paints..circle()..circle()..circle(color: pressedColor))
1094+
: (paints..circle()..circle(color: pressedColor)),
1095+
);
1096+
1097+
await gesture.up();
1098+
await tester.pumpAndSettle();
1099+
1100+
// Press tab to focus the navigation bar.
1101+
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
1102+
await tester.pumpAndSettle();
1103+
1104+
// Test focused state.
1105+
expect(
1106+
inkFeatures,
1107+
kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)),
1108+
);
1109+
});
1110+
10171111
group('Material 2', () {
10181112
// These tests are only relevant for Material 2. Once Material 2
10191113
// support is deprecated and the APIs are removed, these tests

0 commit comments

Comments
 (0)