mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add ability to customize NavigationBar indicator overlay and fix indicator shape for the overlay (#138901)
fixes [Provide ability to override `NavigationBar` indicator ink response overlay](https://github.com/flutter/flutter/issues/138850) fixes [`NavigationBar.indicatorShape` is ignored, `NavigationBarThemeData.indicatorShape` is applied to the indicator inkwell](https://github.com/flutter/flutter/issues/138900) ### 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  #### Indicator shape is ignored for the indicator overlay  ### After #### Can use `NavigationBar.overlayColor` or `NavigationBarThemeData.NavigationBar` to override default indicator overlay `overlayColor: MaterialStatePropertyAll<Color>(Colors.red.withOpacity(0.33)),`  `overlayColor: MaterialStatePropertyAll<Color>(Colors.transparent),`  #### Indicator shape is respected for the indicator overlay 
This commit is contained in:
parent
7b80797d0f
commit
397fd25be1
@ -104,6 +104,7 @@ class NavigationBar extends StatelessWidget {
|
||||
this.indicatorShape,
|
||||
this.height,
|
||||
this.labelBehavior,
|
||||
this.overlayColor,
|
||||
}) : assert(destinations.length >= 2),
|
||||
assert(0 <= selectedIndex && selectedIndex < destinations.length);
|
||||
|
||||
@ -207,6 +208,10 @@ class NavigationBar extends StatelessWidget {
|
||||
/// [NavigationDestinationLabelBehavior.alwaysShow].
|
||||
final NavigationDestinationLabelBehavior? labelBehavior;
|
||||
|
||||
/// The highlight color that's typically used to indicate that
|
||||
/// the [NavigationDestination] is focused, hovered, or pressed.
|
||||
final MaterialStateProperty<Color?>? overlayColor;
|
||||
|
||||
VoidCallback _handleTap(int index) {
|
||||
return onDestinationSelected != null
|
||||
? () => onDestinationSelected!(index)
|
||||
@ -249,6 +254,7 @@ class NavigationBar extends StatelessWidget {
|
||||
labelBehavior: effectiveLabelBehavior,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
overlayColor: overlayColor,
|
||||
onTap: _handleTap(i),
|
||||
child: destinations[i],
|
||||
);
|
||||
@ -509,7 +515,8 @@ class _NavigationDestinationBuilderState extends State<_NavigationDestinationBui
|
||||
child: _IndicatorInkWell(
|
||||
iconKey: iconKey,
|
||||
labelBehavior: info.labelBehavior,
|
||||
customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
|
||||
customBorder: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
|
||||
overlayColor: info.overlayColor ?? navigationBarTheme.overlayColor,
|
||||
onTap: widget.enabled ? info.onTap : null,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@ -532,6 +539,7 @@ class _IndicatorInkWell extends InkResponse {
|
||||
const _IndicatorInkWell({
|
||||
required this.iconKey,
|
||||
required this.labelBehavior,
|
||||
super.overlayColor,
|
||||
super.customBorder,
|
||||
super.onTap,
|
||||
super.child,
|
||||
@ -569,6 +577,7 @@ class _NavigationDestinationInfo extends InheritedWidget {
|
||||
required this.labelBehavior,
|
||||
required this.indicatorColor,
|
||||
required this.indicatorShape,
|
||||
required this.overlayColor,
|
||||
required this.onTap,
|
||||
required super.child,
|
||||
});
|
||||
@ -635,6 +644,12 @@ class _NavigationDestinationInfo extends InheritedWidget {
|
||||
/// This is used by destinations to override the indicator shape.
|
||||
final ShapeBorder? indicatorShape;
|
||||
|
||||
/// The highlight color that's typically used to indicate that
|
||||
/// the [NavigationDestination] is focused, hovered, or pressed.
|
||||
///
|
||||
/// This is used by destinations to override the overlay color.
|
||||
final MaterialStateProperty<Color?>? overlayColor;
|
||||
|
||||
/// The callback that should be called when this destination is tapped.
|
||||
///
|
||||
/// This is computed by calling [NavigationBar.onDestinationSelected]
|
||||
|
||||
@ -52,6 +52,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
this.labelTextStyle,
|
||||
this.iconTheme,
|
||||
this.labelBehavior,
|
||||
this.overlayColor,
|
||||
});
|
||||
|
||||
/// Overrides the default value of [NavigationBar.height].
|
||||
@ -91,6 +92,9 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
/// Overrides the default value of [NavigationBar.labelBehavior].
|
||||
final NavigationDestinationLabelBehavior? labelBehavior;
|
||||
|
||||
/// Overrides the default value of [NavigationBar.overlayColor].
|
||||
final MaterialStateProperty<Color?>? overlayColor;
|
||||
|
||||
/// Creates a copy of this object with the given fields replaced with the
|
||||
/// new values.
|
||||
NavigationBarThemeData copyWith({
|
||||
@ -104,6 +108,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
MaterialStateProperty<TextStyle?>? labelTextStyle,
|
||||
MaterialStateProperty<IconThemeData?>? iconTheme,
|
||||
NavigationDestinationLabelBehavior? labelBehavior,
|
||||
MaterialStateProperty<Color?>? overlayColor,
|
||||
}) {
|
||||
return NavigationBarThemeData(
|
||||
height: height ?? this.height,
|
||||
@ -116,6 +121,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
|
||||
iconTheme: iconTheme ?? this.iconTheme,
|
||||
labelBehavior: labelBehavior ?? this.labelBehavior,
|
||||
overlayColor: overlayColor ?? this.overlayColor,
|
||||
);
|
||||
}
|
||||
|
||||
@ -139,6 +145,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
labelTextStyle: MaterialStateProperty.lerp<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
|
||||
iconTheme: MaterialStateProperty.lerp<IconThemeData?>(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
|
||||
labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior,
|
||||
overlayColor: MaterialStateProperty.lerp<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
@ -154,6 +161,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
labelTextStyle,
|
||||
iconTheme,
|
||||
labelBehavior,
|
||||
overlayColor,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -165,16 +173,17 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
return false;
|
||||
}
|
||||
return other is NavigationBarThemeData
|
||||
&& other.height == height
|
||||
&& other.backgroundColor == backgroundColor
|
||||
&& other.elevation == elevation
|
||||
&& other.shadowColor == shadowColor
|
||||
&& other.surfaceTintColor == surfaceTintColor
|
||||
&& other.indicatorColor == indicatorColor
|
||||
&& other.indicatorShape == indicatorShape
|
||||
&& other.labelTextStyle == labelTextStyle
|
||||
&& other.iconTheme == iconTheme
|
||||
&& other.labelBehavior == labelBehavior;
|
||||
&& other.height == height
|
||||
&& other.backgroundColor == backgroundColor
|
||||
&& other.elevation == elevation
|
||||
&& other.shadowColor == shadowColor
|
||||
&& other.surfaceTintColor == surfaceTintColor
|
||||
&& other.indicatorColor == indicatorColor
|
||||
&& other.indicatorShape == indicatorShape
|
||||
&& other.labelTextStyle == labelTextStyle
|
||||
&& other.iconTheme == iconTheme
|
||||
&& other.labelBehavior == labelBehavior
|
||||
&& other.overlayColor == overlayColor;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -190,6 +199,7 @@ class NavigationBarThemeData with Diagnosticable {
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>('iconTheme', iconTheme, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<NavigationDestinationLabelBehavior>('labelBehavior', labelBehavior, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||
|
||||
@ -937,7 +938,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('Material3 - Navigation destination updates indicator color and shape', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final ThemeData theme = ThemeData();
|
||||
const Color color = Color(0xff0000ff);
|
||||
const ShapeBorder shape = RoundedRectangleBorder();
|
||||
|
||||
@ -945,20 +946,22 @@ void main() {
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Scaffold(
|
||||
bottomNavigationBar: NavigationBar(
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) { },
|
||||
bottomNavigationBar: RepaintBoundary(
|
||||
child: NavigationBar(
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -970,11 +973,22 @@ void main() {
|
||||
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
|
||||
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Test default indicator color and shape with ripple.
|
||||
await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.default.indicator.inkwell.shape.png'));
|
||||
|
||||
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
|
||||
|
||||
// Test custom indicator color and shape.
|
||||
expect(_getIndicatorDecoration(tester)?.color, color);
|
||||
expect(_getIndicatorDecoration(tester)?.shape, shape);
|
||||
|
||||
// Test custom indicator color and shape with ripple.
|
||||
await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.custom.indicator.inkwell.shape.png'));
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async {
|
||||
@ -1014,6 +1028,86 @@ void main() {
|
||||
expect(selectedIndex, 1);
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('NavigationBar respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
const Color hoverColor = Color(0xff0000ff);
|
||||
const Color focusColor = Color(0xff00ffff);
|
||||
const Color pressedColor = Color(0xffff00ff);
|
||||
final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return hoverColor;
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return focusColor;
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return pressedColor;
|
||||
}
|
||||
return Colors.transparent;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
bottomNavigationBar: RepaintBoundary(
|
||||
child: NavigationBar(
|
||||
overlayColor: overlayColor,
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||
|
||||
// Test hovered state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb
|
||||
? (paints..rrect()..rrect()..circle(color: hoverColor))
|
||||
: (paints..circle(color: hoverColor)),
|
||||
);
|
||||
|
||||
await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Test pressed state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb
|
||||
? (paints..circle()..circle()..circle(color: pressedColor))
|
||||
: (paints..circle()..circle(color: pressedColor)),
|
||||
);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Press tab to focus the navigation bar.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Test focused state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)),
|
||||
);
|
||||
});
|
||||
|
||||
group('Material 2', () {
|
||||
// These tests are only relevant for Material 2. Once Material 2
|
||||
// support is deprecated and the APIs are removed, these tests
|
||||
|
||||
@ -7,9 +7,11 @@
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||
|
||||
@ -48,6 +50,7 @@ void main() {
|
||||
labelTextStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)),
|
||||
iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000097))),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
overlayColor: MaterialStatePropertyAll<Color>(Color(0x00000096)),
|
||||
).debugFillProperties(builder);
|
||||
|
||||
final List<String> description = builder.properties
|
||||
@ -61,12 +64,11 @@ void main() {
|
||||
expect(description[3], 'indicatorColor: Color(0x00000098)');
|
||||
expect(description[4], 'indicatorShape: CircleBorder(BorderSide(width: 0.0, style: none))');
|
||||
expect(description[5], 'labelTextStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 7.0))');
|
||||
|
||||
// Ignore instance address for IconThemeData.
|
||||
expect(description[6].contains('iconTheme: MaterialStatePropertyAll(IconThemeData'), isTrue);
|
||||
expect(description[6].contains('(color: Color(0x00000097))'), isTrue);
|
||||
|
||||
expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
|
||||
expect(description[8], 'overlayColor: MaterialStatePropertyAll(Color(0x00000096))');
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async {
|
||||
@ -216,6 +218,86 @@ void main() {
|
||||
|
||||
await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_custom_label_style.png'));
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('NavigationBar respects NavigationBarTheme.overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
const Color hoverColor = Color(0xff0000ff);
|
||||
const Color focusColor = Color(0xff00ffff);
|
||||
const Color pressedColor = Color(0xffff00ff);
|
||||
final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return hoverColor;
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return focusColor;
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return pressedColor;
|
||||
}
|
||||
return Colors.transparent;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(navigationBarTheme: NavigationBarThemeData(overlayColor: overlayColor)),
|
||||
home: Scaffold(
|
||||
bottomNavigationBar: RepaintBoundary(
|
||||
child: NavigationBar(
|
||||
destinations: const <Widget>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.ac_unit),
|
||||
label: 'AC',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.access_alarm),
|
||||
label: 'Alarm',
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (int i) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||
|
||||
// Test hovered state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb
|
||||
? (paints..rrect()..rrect()..circle(color: hoverColor))
|
||||
: (paints..circle(color: hoverColor)),
|
||||
);
|
||||
|
||||
await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Test pressed state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb
|
||||
? (paints..circle()..circle()..circle(color: pressedColor))
|
||||
: (paints..circle()..circle(color: pressedColor)),
|
||||
);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Press tab to focus the navigation bar.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Test focused state.
|
||||
expect(
|
||||
inkFeatures,
|
||||
kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<NavigationDestination> _destinations() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user