`NavigationBar` improvements (#116992)
diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.0.dart b/examples/api/lib/material/navigation_bar/navigation_bar.0.dart
index ac22535..41cca1b 100644
--- a/examples/api/lib/material/navigation_bar/navigation_bar.0.dart
+++ b/examples/api/lib/material/navigation_bar/navigation_bar.0.dart
@@ -6,10 +6,10 @@
import 'package:flutter/material.dart';
-void main() => runApp(const ExampleApp());
+void main() => runApp(const NavigationBarApp());
-class ExampleApp extends StatelessWidget {
- const ExampleApp({super.key});
+class NavigationBarApp extends StatelessWidget {
+ const NavigationBarApp({super.key});
@override
Widget build(BuildContext context) {
diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.1.dart b/examples/api/lib/material/navigation_bar/navigation_bar.1.dart
index 42d21aa..06191a7 100644
--- a/examples/api/lib/material/navigation_bar/navigation_bar.1.dart
+++ b/examples/api/lib/material/navigation_bar/navigation_bar.1.dart
@@ -2,223 +2,93 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-/// Flutter code sample for [NavigationBar] with nested [Navigator] destinations.
+/// Flutter code sample for [NavigationBar].
import 'package:flutter/material.dart';
-void main() {
- runApp(const MaterialApp(home: Home()));
-}
+void main() => runApp(const NavigationBarApp());
-class Home extends StatefulWidget {
- const Home({ super.key });
-
- @override
- State<Home> createState() => _HomeState();
-}
-
-class _HomeState extends State<Home> with TickerProviderStateMixin<Home> {
- static const List<Destination> allDestinations = <Destination>[
- Destination(0, 'Teal', Icons.home, Colors.teal),
- Destination(1, 'Cyan', Icons.business, Colors.cyan),
- Destination(2, 'Orange', Icons.school, Colors.orange),
- Destination(3, 'Blue', Icons.flight, Colors.blue),
- ];
-
- late final List<GlobalKey<NavigatorState>> navigatorKeys;
- late final List<GlobalKey> destinationKeys;
- late final List<AnimationController> destinationFaders;
- late final List<Widget> destinationViews;
- int selectedIndex = 0;
-
- AnimationController buildFaderController() {
- final AnimationController controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
- controller.addStatusListener((AnimationStatus status) {
- if (status == AnimationStatus.dismissed) {
- setState(() { }); // Rebuild unselected destinations offstage.
- }
- });
- return controller;
- }
-
- @override
- void initState() {
- super.initState();
- navigatorKeys = List<GlobalKey<NavigatorState>>.generate(allDestinations.length, (int index) => GlobalKey()).toList();
- destinationFaders = List<AnimationController>.generate(allDestinations.length, (int index) => buildFaderController()).toList();
- destinationFaders[selectedIndex].value = 1.0;
- destinationViews = allDestinations.map((Destination destination) {
- return FadeTransition(
- opacity: destinationFaders[destination.index].drive(CurveTween(curve: Curves.fastOutSlowIn)),
- child: DestinationView(
- destination: destination,
- navigatorKey: navigatorKeys[destination.index],
- )
- );
- }).toList();
- }
-
- @override
- void dispose() {
- for (final AnimationController controller in destinationFaders) {
- controller.dispose();
- }
- super.dispose();
- }
+class NavigationBarApp extends StatelessWidget {
+ const NavigationBarApp({super.key});
@override
Widget build(BuildContext context) {
- return WillPopScope(
- onWillPop: () async {
- final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
- if (!navigator.canPop()) {
- return true;
- }
- navigator.pop();
- return false;
- },
- child: Scaffold(
- body: SafeArea(
- top: false,
- child: Stack(
- fit: StackFit.expand,
- children: allDestinations.map((Destination destination) {
- final int index = destination.index;
- final Widget view = destinationViews[index];
- if (index == selectedIndex) {
- destinationFaders[index].forward();
- return Offstage(offstage: false, child: view);
- } else {
- destinationFaders[index].reverse();
- if (destinationFaders[index].isAnimating) {
- return IgnorePointer(child: view);
- }
- return Offstage(child: view);
- }
- }).toList(),
- ),
- ),
- bottomNavigationBar: NavigationBar(
- selectedIndex: selectedIndex,
- onDestinationSelected: (int index) {
- setState(() {
- selectedIndex = index;
- });
- },
- destinations: allDestinations.map((Destination destination) {
- return NavigationDestination(
- icon: Icon(destination.icon, color: destination.color),
- label: destination.title,
- );
- }).toList(),
- ),
- ),
- );
+ return const MaterialApp(home: NavigationExample());
}
}
-class Destination {
- const Destination(this.index, this.title, this.icon, this.color);
- final int index;
- final String title;
- final IconData icon;
- final MaterialColor color;
+class NavigationExample extends StatefulWidget {
+ const NavigationExample({super.key});
+
+ @override
+ State<NavigationExample> createState() => _NavigationExampleState();
}
-class RootPage extends StatelessWidget {
- const RootPage({ super.key, required this.destination });
-
- final Destination destination;
-
- Widget _buildDialog(BuildContext context) {
- return AlertDialog(
- title: Text('${destination.title} AlertDialog'),
- actions: <Widget>[
- TextButton(
- onPressed: () { Navigator.pop(context); },
- child: const Text('OK'),
- ),
- ],
- );
- }
+class _NavigationExampleState extends State<NavigationExample> {
+ int currentPageIndex = 0;
+ NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
@override
Widget build(BuildContext context) {
- final TextStyle headlineSmall = Theme.of(context).textTheme.headlineSmall!;
- final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
- backgroundColor: destination.color,
- visualDensity: VisualDensity.comfortable,
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
- textStyle: headlineSmall,
- );
-
return Scaffold(
- appBar: AppBar(
- title: Text('${destination.title} RootPage - /'),
- backgroundColor: destination.color,
+ bottomNavigationBar: NavigationBar(
+ labelBehavior: labelBehavior,
+ selectedIndex: currentPageIndex,
+ onDestinationSelected: (int index) {
+ setState(() {
+ currentPageIndex = index;
+ });
+ },
+ destinations: const <Widget>[
+ NavigationDestination(
+ icon: Icon(Icons.explore),
+ label: 'Explore',
+ ),
+ NavigationDestination(
+ icon: Icon(Icons.commute),
+ label: 'Commute',
+ ),
+ NavigationDestination(
+ selectedIcon: Icon(Icons.bookmark),
+ icon: Icon(Icons.bookmark_border),
+ label: 'Saved',
+ ),
+ ],
),
- backgroundColor: destination.color[50],
body: Center(
child: Column(
- mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
- ElevatedButton(
- style: buttonStyle,
- onPressed: () {
- Navigator.pushNamed(context, '/list');
- },
- child: const Text('Push /list'),
- ),
- const SizedBox(height: 16),
- ElevatedButton(
- style: buttonStyle,
- onPressed: () {
- showDialog(
- context: context,
- useRootNavigator: false,
- builder: _buildDialog,
- );
- },
- child: const Text('Local Dialog'),
- ),
- const SizedBox(height: 16),
- ElevatedButton(
- style: buttonStyle,
- onPressed: () {
- showDialog(
- context: context,
- useRootNavigator: true,
- builder: _buildDialog,
- );
- },
- child: const Text('Root Dialog'),
- ),
- const SizedBox(height: 16),
- Builder(
- builder: (BuildContext context) {
- return ElevatedButton(
- style: buttonStyle,
+ Text('Label behavior: ${labelBehavior.name}'),
+ const SizedBox(height: 10),
+ OverflowBar(
+ spacing: 10.0,
+ children: <Widget>[
+ ElevatedButton(
onPressed: () {
- showBottomSheet(
- context: context,
- builder: (BuildContext context) {
- return Container(
- padding: const EdgeInsets.all(16),
- width: double.infinity,
- child: Text(
- '${destination.title} BottomSheet\n'
- 'Tap the back button to dismiss',
- style: headlineSmall,
- softWrap: true,
- textAlign: TextAlign.center,
- ),
- );
- },
- );
+ setState(() {
+ labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
+ });
},
- child: const Text('Local BottomSheet'),
- );
- },
+ child: const Text('alwaysShow'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ labelBehavior = NavigationDestinationLabelBehavior.onlyShowSelected;
+ });
+ },
+ child: const Text('onlyShowSelected'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ labelBehavior = NavigationDestinationLabelBehavior.alwaysHide;
+ });
+ },
+ child: const Text('alwaysHide'),
+ ),
+ ],
),
],
),
@@ -226,142 +96,3 @@
);
}
}
-
-class ListPage extends StatelessWidget {
- const ListPage({ super.key, required this.destination });
-
- final Destination destination;
-
- @override
- Widget build(BuildContext context) {
- const int itemCount = 50;
- final ButtonStyle buttonStyle = OutlinedButton.styleFrom(
- foregroundColor: destination.color,
- fixedSize: const Size.fromHeight(128),
- textStyle: Theme.of(context).textTheme.headlineSmall,
- );
- return Scaffold(
- appBar: AppBar(
- title: Text('${destination.title} ListPage - /list'),
- backgroundColor: destination.color,
- ),
- backgroundColor: destination.color[50],
- body: SizedBox.expand(
- child: ListView.builder(
- itemCount: itemCount,
- itemBuilder: (BuildContext context, int index) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
- child: OutlinedButton(
- style: buttonStyle.copyWith(
- backgroundColor: MaterialStatePropertyAll<Color>(
- Color.lerp(destination.color[100], Colors.white, index / itemCount)!
- ),
- ),
- onPressed: () {
- Navigator.pushNamed(context, '/text');
- },
- child: Text('Push /text [$index]'),
- ),
- );
- },
- ),
- ),
- );
- }
-}
-
-class TextPage extends StatefulWidget {
- const TextPage({ super.key, required this.destination });
-
- final Destination destination;
-
- @override
- State<TextPage> createState() => _TextPageState();
-}
-
-class _TextPageState extends State<TextPage> {
- late final TextEditingController textController;
-
- @override
- void initState() {
- super.initState();
- textController = TextEditingController(text: 'Sample Text');
- }
-
- @override
- void dispose() {
- textController.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- return Scaffold(
- appBar: AppBar(
- title: Text('${widget.destination.title} TextPage - /list/text'),
- backgroundColor: widget.destination.color,
- ),
- backgroundColor: widget.destination.color[50],
- body: Container(
- padding: const EdgeInsets.all(32.0),
- alignment: Alignment.center,
- child: TextField(
- controller: textController,
- style: theme.primaryTextTheme.headlineMedium?.copyWith(
- color: widget.destination.color,
- ),
- decoration: InputDecoration(
- focusedBorder: UnderlineInputBorder(
- borderSide: BorderSide(
- color: widget.destination.color,
- width: 3.0,
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
-
-class DestinationView extends StatefulWidget {
- const DestinationView({
- super.key,
- required this.destination,
- required this.navigatorKey,
- });
-
- final Destination destination;
- final Key navigatorKey;
-
- @override
- State<DestinationView> createState() => _DestinationViewState();
-}
-
-class _DestinationViewState extends State<DestinationView> {
- @override
- Widget build(BuildContext context) {
- return Navigator(
- key: widget.navigatorKey,
- onGenerateRoute: (RouteSettings settings) {
- return MaterialPageRoute<void>(
- settings: settings,
- builder: (BuildContext context) {
- switch(settings.name) {
- case '/':
- return RootPage(destination: widget.destination);
- case '/list':
- return ListPage(destination: widget.destination);
- case '/text':
- return TextPage(destination: widget.destination);
- }
- assert(false);
- return const SizedBox();
- },
- );
- },
- );
- }
-}
diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart
new file mode 100644
index 0000000..42d21aa
--- /dev/null
+++ b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart
@@ -0,0 +1,367 @@
+// 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.
+
+/// Flutter code sample for [NavigationBar] with nested [Navigator] destinations.
+
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const MaterialApp(home: Home()));
+}
+
+class Home extends StatefulWidget {
+ const Home({ super.key });
+
+ @override
+ State<Home> createState() => _HomeState();
+}
+
+class _HomeState extends State<Home> with TickerProviderStateMixin<Home> {
+ static const List<Destination> allDestinations = <Destination>[
+ Destination(0, 'Teal', Icons.home, Colors.teal),
+ Destination(1, 'Cyan', Icons.business, Colors.cyan),
+ Destination(2, 'Orange', Icons.school, Colors.orange),
+ Destination(3, 'Blue', Icons.flight, Colors.blue),
+ ];
+
+ late final List<GlobalKey<NavigatorState>> navigatorKeys;
+ late final List<GlobalKey> destinationKeys;
+ late final List<AnimationController> destinationFaders;
+ late final List<Widget> destinationViews;
+ int selectedIndex = 0;
+
+ AnimationController buildFaderController() {
+ final AnimationController controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
+ controller.addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.dismissed) {
+ setState(() { }); // Rebuild unselected destinations offstage.
+ }
+ });
+ return controller;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ navigatorKeys = List<GlobalKey<NavigatorState>>.generate(allDestinations.length, (int index) => GlobalKey()).toList();
+ destinationFaders = List<AnimationController>.generate(allDestinations.length, (int index) => buildFaderController()).toList();
+ destinationFaders[selectedIndex].value = 1.0;
+ destinationViews = allDestinations.map((Destination destination) {
+ return FadeTransition(
+ opacity: destinationFaders[destination.index].drive(CurveTween(curve: Curves.fastOutSlowIn)),
+ child: DestinationView(
+ destination: destination,
+ navigatorKey: navigatorKeys[destination.index],
+ )
+ );
+ }).toList();
+ }
+
+ @override
+ void dispose() {
+ for (final AnimationController controller in destinationFaders) {
+ controller.dispose();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return WillPopScope(
+ onWillPop: () async {
+ final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
+ if (!navigator.canPop()) {
+ return true;
+ }
+ navigator.pop();
+ return false;
+ },
+ child: Scaffold(
+ body: SafeArea(
+ top: false,
+ child: Stack(
+ fit: StackFit.expand,
+ children: allDestinations.map((Destination destination) {
+ final int index = destination.index;
+ final Widget view = destinationViews[index];
+ if (index == selectedIndex) {
+ destinationFaders[index].forward();
+ return Offstage(offstage: false, child: view);
+ } else {
+ destinationFaders[index].reverse();
+ if (destinationFaders[index].isAnimating) {
+ return IgnorePointer(child: view);
+ }
+ return Offstage(child: view);
+ }
+ }).toList(),
+ ),
+ ),
+ bottomNavigationBar: NavigationBar(
+ selectedIndex: selectedIndex,
+ onDestinationSelected: (int index) {
+ setState(() {
+ selectedIndex = index;
+ });
+ },
+ destinations: allDestinations.map((Destination destination) {
+ return NavigationDestination(
+ icon: Icon(destination.icon, color: destination.color),
+ label: destination.title,
+ );
+ }).toList(),
+ ),
+ ),
+ );
+ }
+}
+
+class Destination {
+ const Destination(this.index, this.title, this.icon, this.color);
+ final int index;
+ final String title;
+ final IconData icon;
+ final MaterialColor color;
+}
+
+class RootPage extends StatelessWidget {
+ const RootPage({ super.key, required this.destination });
+
+ final Destination destination;
+
+ Widget _buildDialog(BuildContext context) {
+ return AlertDialog(
+ title: Text('${destination.title} AlertDialog'),
+ actions: <Widget>[
+ TextButton(
+ onPressed: () { Navigator.pop(context); },
+ child: const Text('OK'),
+ ),
+ ],
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle headlineSmall = Theme.of(context).textTheme.headlineSmall!;
+ final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
+ backgroundColor: destination.color,
+ visualDensity: VisualDensity.comfortable,
+ padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ textStyle: headlineSmall,
+ );
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('${destination.title} RootPage - /'),
+ backgroundColor: destination.color,
+ ),
+ backgroundColor: destination.color[50],
+ body: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ ElevatedButton(
+ style: buttonStyle,
+ onPressed: () {
+ Navigator.pushNamed(context, '/list');
+ },
+ child: const Text('Push /list'),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ style: buttonStyle,
+ onPressed: () {
+ showDialog(
+ context: context,
+ useRootNavigator: false,
+ builder: _buildDialog,
+ );
+ },
+ child: const Text('Local Dialog'),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ style: buttonStyle,
+ onPressed: () {
+ showDialog(
+ context: context,
+ useRootNavigator: true,
+ builder: _buildDialog,
+ );
+ },
+ child: const Text('Root Dialog'),
+ ),
+ const SizedBox(height: 16),
+ Builder(
+ builder: (BuildContext context) {
+ return ElevatedButton(
+ style: buttonStyle,
+ onPressed: () {
+ showBottomSheet(
+ context: context,
+ builder: (BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ width: double.infinity,
+ child: Text(
+ '${destination.title} BottomSheet\n'
+ 'Tap the back button to dismiss',
+ style: headlineSmall,
+ softWrap: true,
+ textAlign: TextAlign.center,
+ ),
+ );
+ },
+ );
+ },
+ child: const Text('Local BottomSheet'),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ListPage extends StatelessWidget {
+ const ListPage({ super.key, required this.destination });
+
+ final Destination destination;
+
+ @override
+ Widget build(BuildContext context) {
+ const int itemCount = 50;
+ final ButtonStyle buttonStyle = OutlinedButton.styleFrom(
+ foregroundColor: destination.color,
+ fixedSize: const Size.fromHeight(128),
+ textStyle: Theme.of(context).textTheme.headlineSmall,
+ );
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('${destination.title} ListPage - /list'),
+ backgroundColor: destination.color,
+ ),
+ backgroundColor: destination.color[50],
+ body: SizedBox.expand(
+ child: ListView.builder(
+ itemCount: itemCount,
+ itemBuilder: (BuildContext context, int index) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
+ child: OutlinedButton(
+ style: buttonStyle.copyWith(
+ backgroundColor: MaterialStatePropertyAll<Color>(
+ Color.lerp(destination.color[100], Colors.white, index / itemCount)!
+ ),
+ ),
+ onPressed: () {
+ Navigator.pushNamed(context, '/text');
+ },
+ child: Text('Push /text [$index]'),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class TextPage extends StatefulWidget {
+ const TextPage({ super.key, required this.destination });
+
+ final Destination destination;
+
+ @override
+ State<TextPage> createState() => _TextPageState();
+}
+
+class _TextPageState extends State<TextPage> {
+ late final TextEditingController textController;
+
+ @override
+ void initState() {
+ super.initState();
+ textController = TextEditingController(text: 'Sample Text');
+ }
+
+ @override
+ void dispose() {
+ textController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('${widget.destination.title} TextPage - /list/text'),
+ backgroundColor: widget.destination.color,
+ ),
+ backgroundColor: widget.destination.color[50],
+ body: Container(
+ padding: const EdgeInsets.all(32.0),
+ alignment: Alignment.center,
+ child: TextField(
+ controller: textController,
+ style: theme.primaryTextTheme.headlineMedium?.copyWith(
+ color: widget.destination.color,
+ ),
+ decoration: InputDecoration(
+ focusedBorder: UnderlineInputBorder(
+ borderSide: BorderSide(
+ color: widget.destination.color,
+ width: 3.0,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class DestinationView extends StatefulWidget {
+ const DestinationView({
+ super.key,
+ required this.destination,
+ required this.navigatorKey,
+ });
+
+ final Destination destination;
+ final Key navigatorKey;
+
+ @override
+ State<DestinationView> createState() => _DestinationViewState();
+}
+
+class _DestinationViewState extends State<DestinationView> {
+ @override
+ Widget build(BuildContext context) {
+ return Navigator(
+ key: widget.navigatorKey,
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) {
+ switch(settings.name) {
+ case '/':
+ return RootPage(destination: widget.destination);
+ case '/list':
+ return ListPage(destination: widget.destination);
+ case '/text':
+ return TextPage(destination: widget.destination);
+ }
+ assert(false);
+ return const SizedBox();
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart b/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart
index 501058e..b786889 100644
--- a/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart
+++ b/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart
@@ -11,10 +11,9 @@
testWidgets('Navigation bar updates destination on tap',
(WidgetTester tester) async {
await tester.pumpWidget(
- const example.ExampleApp(),
+ const example.NavigationBarApp(),
);
- final NavigationBar navigationBarWidget =
- tester.firstWidget(find.byType(NavigationBar));
+ final NavigationBar navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
/// NavigationDestinations must be rendered
expect(find.text('Explore'), findsOneWidget);
diff --git a/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart b/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart
index 23c35f9..1300565 100644
--- a/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart
+++ b/examples/api/test/material/navigation_bar/navigation_bar.1_test.dart
@@ -3,106 +3,41 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
-import 'package:flutter_api_samples/material/navigation_bar/navigation_bar.1.dart' as example;
+import 'package:flutter_api_samples/material/navigation_bar/navigation_bar.1.dart'
+ as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
- testWidgets('RootPage: only selected destination is on stage', (WidgetTester tester) async {
- await tester.pumpWidget(const MaterialApp(home: example.Home()));
+ testWidgets('Navigation bar updates label behavior when tapping buttons',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const example.NavigationBarApp(),
+ );
+ NavigationBar navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
- const String tealTitle = 'Teal RootPage - /';
- const String cyanTitle = 'Cyan RootPage - /';
- const String orangeTitle = 'Orange RootPage - /';
- const String blueTitle = 'Blue RootPage - /';
+ expect(find.text('Label behavior: alwaysShow'), findsOneWidget);
- await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
+ /// Test alwaysShow label behavior button.
+ await tester.tap(find.widgetWithText(ElevatedButton, 'alwaysShow'));
await tester.pumpAndSettle();
- expect(find.text(tealTitle), findsOneWidget);
- expect(find.text(cyanTitle), findsNothing);
- expect(find.text(orangeTitle), findsNothing);
- expect(find.text(blueTitle), findsNothing);
- await tester.tap(find.widgetWithText(NavigationDestination, 'Cyan'));
- await tester.pumpAndSettle();
- expect(find.text(tealTitle), findsNothing);
- expect(find.text(cyanTitle), findsOneWidget);
- expect(find.text(orangeTitle), findsNothing);
- expect(find.text(blueTitle), findsNothing);
+ expect(find.text('Label behavior: alwaysShow'), findsOneWidget);
+ expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.alwaysShow);
- await tester.tap(find.widgetWithText(NavigationDestination, 'Orange'));
+ /// Test onlyShowSelected label behavior button.
+ await tester.tap(find.widgetWithText(ElevatedButton, 'onlyShowSelected'));
await tester.pumpAndSettle();
- expect(find.text(tealTitle), findsNothing);
- expect(find.text(cyanTitle), findsNothing);
- expect(find.text(orangeTitle), findsOneWidget);
- expect(find.text(blueTitle), findsNothing);
- await tester.tap(find.widgetWithText(NavigationDestination, 'Blue'));
- await tester.pumpAndSettle();
- expect(find.text(tealTitle), findsNothing);
- expect(find.text(cyanTitle), findsNothing);
- expect(find.text(orangeTitle), findsNothing);
- expect(find.text(blueTitle), findsOneWidget);
- });
+ expect(find.text('Label behavior: onlyShowSelected'), findsOneWidget);
+ navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
+ expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.onlyShowSelected);
- testWidgets('RootPage', (WidgetTester tester) async {
- await tester.pumpWidget(const MaterialApp(home: example.Home()));
+ /// Test alwaysHide label behavior button.
+ await tester.tap(find.widgetWithText(ElevatedButton, 'alwaysHide'));
+ await tester.pumpAndSettle();
- await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
- await tester.pumpAndSettle();
- await tester.tap(find.text('Local Dialog'));
- await tester.pumpAndSettle();
- expect(find.text('Teal AlertDialog'), findsOneWidget);
- await tester.tap(find.text('OK'));
- await tester.pumpAndSettle();
- expect(find.text('Teal AlertDialog'), findsNothing);
-
- await tester.pumpAndSettle();
- await tester.tap(find.text('Root Dialog'));
- await tester.pumpAndSettle();
- expect(find.text('Teal AlertDialog'), findsOneWidget);
- await tester.tapAt(const Offset(5, 5));
- await tester.pumpAndSettle();
- expect(find.text('Teal AlertDialog'), findsNothing);
-
- await tester.tap(find.text('Local BottomSheet'));
- await tester.pumpAndSettle();
- expect(find.byType(BottomSheet), findsOneWidget);
- await tester.tap(find.byType(BackButton));
- await tester.pumpAndSettle();
- expect(find.byType(BottomSheet), findsNothing);
-
- await tester.tap(find.text('Push /list'));
- await tester.pumpAndSettle();
- expect(find.text('Teal ListPage - /list'), findsOneWidget);
- });
-
-
- testWidgets('ListPage', (WidgetTester tester) async {
- await tester.pumpWidget(const MaterialApp(home: example.Home()));
- expect(find.text('Teal RootPage - /'), findsOneWidget);
-
- await tester.tap(find.widgetWithText(ElevatedButton, 'Push /list'));
- await tester.pumpAndSettle();
- expect(find.text('Teal ListPage - /list'), findsOneWidget);
- expect(find.text('Push /text [0]'), findsOneWidget);
-
- await tester.tap(find.widgetWithText(NavigationDestination, 'Orange'));
- await tester.pumpAndSettle();
- await tester.tap(find.widgetWithText(ElevatedButton, 'Push /list'));
- await tester.pumpAndSettle();
- expect(find.text('Orange ListPage - /list'), findsOneWidget);
- expect(find.text('Push /text [0]'), findsOneWidget);
-
- await tester.tap(find.byType(BackButton));
- await tester.pumpAndSettle();
- expect(find.text('Orange RootPage - /'), findsOneWidget);
-
- await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
- await tester.pumpAndSettle();
- expect(find.text('Teal ListPage - /list'), findsOneWidget);
-
- await tester.tap(find.byType(BackButton));
- await tester.pumpAndSettle();
- expect(find.text('Teal RootPage - /'), findsOneWidget);
+ expect(find.text('Label behavior: alwaysHide'), findsOneWidget);
+ navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
+ expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.alwaysHide);
});
}
diff --git a/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart b/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart
new file mode 100644
index 0000000..cba7381
--- /dev/null
+++ b/examples/api/test/material/navigation_bar/navigation_bar.2_test.dart
@@ -0,0 +1,108 @@
+// 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 'package:flutter/material.dart';
+import 'package:flutter_api_samples/material/navigation_bar/navigation_bar.2.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('RootPage: only selected destination is on stage', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(home: example.Home()));
+
+ const String tealTitle = 'Teal RootPage - /';
+ const String cyanTitle = 'Cyan RootPage - /';
+ const String orangeTitle = 'Orange RootPage - /';
+ const String blueTitle = 'Blue RootPage - /';
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
+ await tester.pumpAndSettle();
+ expect(find.text(tealTitle), findsOneWidget);
+ expect(find.text(cyanTitle), findsNothing);
+ expect(find.text(orangeTitle), findsNothing);
+ expect(find.text(blueTitle), findsNothing);
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Cyan'));
+ await tester.pumpAndSettle();
+ expect(find.text(tealTitle), findsNothing);
+ expect(find.text(cyanTitle), findsOneWidget);
+ expect(find.text(orangeTitle), findsNothing);
+ expect(find.text(blueTitle), findsNothing);
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Orange'));
+ await tester.pumpAndSettle();
+ expect(find.text(tealTitle), findsNothing);
+ expect(find.text(cyanTitle), findsNothing);
+ expect(find.text(orangeTitle), findsOneWidget);
+ expect(find.text(blueTitle), findsNothing);
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Blue'));
+ await tester.pumpAndSettle();
+ expect(find.text(tealTitle), findsNothing);
+ expect(find.text(cyanTitle), findsNothing);
+ expect(find.text(orangeTitle), findsNothing);
+ expect(find.text(blueTitle), findsOneWidget);
+ });
+
+ testWidgets('RootPage', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(home: example.Home()));
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Local Dialog'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal AlertDialog'), findsOneWidget);
+ await tester.tap(find.text('OK'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal AlertDialog'), findsNothing);
+
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Root Dialog'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal AlertDialog'), findsOneWidget);
+ await tester.tapAt(const Offset(5, 5));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal AlertDialog'), findsNothing);
+
+ await tester.tap(find.text('Local BottomSheet'));
+ await tester.pumpAndSettle();
+ expect(find.byType(BottomSheet), findsOneWidget);
+ await tester.tap(find.byType(BackButton));
+ await tester.pumpAndSettle();
+ expect(find.byType(BottomSheet), findsNothing);
+
+ await tester.tap(find.text('Push /list'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal ListPage - /list'), findsOneWidget);
+ });
+
+
+ testWidgets('ListPage', (WidgetTester tester) async {
+ await tester.pumpWidget(const MaterialApp(home: example.Home()));
+ expect(find.text('Teal RootPage - /'), findsOneWidget);
+
+ await tester.tap(find.widgetWithText(ElevatedButton, 'Push /list'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal ListPage - /list'), findsOneWidget);
+ expect(find.text('Push /text [0]'), findsOneWidget);
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Orange'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.widgetWithText(ElevatedButton, 'Push /list'));
+ await tester.pumpAndSettle();
+ expect(find.text('Orange ListPage - /list'), findsOneWidget);
+ expect(find.text('Push /text [0]'), findsOneWidget);
+
+ await tester.tap(find.byType(BackButton));
+ await tester.pumpAndSettle();
+ expect(find.text('Orange RootPage - /'), findsOneWidget);
+
+ await tester.tap(find.widgetWithText(NavigationDestination, 'Teal'));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal ListPage - /list'), findsOneWidget);
+
+ await tester.tap(find.byType(BackButton));
+ await tester.pumpAndSettle();
+ expect(find.text('Teal RootPage - /'), findsOneWidget);
+ });
+}
diff --git a/packages/flutter/lib/src/material/navigation_bar.dart b/packages/flutter/lib/src/material/navigation_bar.dart
index 0b24712..b17704b 100644
--- a/packages/flutter/lib/src/material/navigation_bar.dart
+++ b/packages/flutter/lib/src/material/navigation_bar.dart
@@ -53,6 +53,14 @@
/// {@end-tool}
///
/// {@tool dartpad}
+/// This example showcases [NavigationBar] label behaviors. When tapping on one
+/// of the label behavior options, the [labelBehavior] of the [NavigationBar]
+/// will be updated.
+///
+/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.1.dart **
+/// {@end-tool}
+///
+/// {@tool dartpad}
/// This example shows a [NavigationBar] as it is used within a [Scaffold]
/// widget when there are nested navigators that provide local navigation. The
/// [NavigationBar] has four [NavigationDestination] widgets with different
@@ -60,7 +68,7 @@
/// item's index and displays a corresponding page with its own local navigator
/// in the body of a [Scaffold].
///
-/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.1.dart **
+/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.2.dart **
/// {@end-tool}
/// See also:
///
@@ -266,6 +274,8 @@
super.key,
required this.icon,
this.selectedIcon,
+ this.indicatorColor,
+ this.indicatorShape,
required this.label,
this.tooltip,
});
@@ -290,6 +300,12 @@
/// 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].
///
@@ -335,8 +351,8 @@
children: <Widget>[
NavigationIndicator(
animation: animation,
- color: navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
- shape: navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
+ color: indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
+ shape: indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
),
_StatusTransitionWidgetBuilder(
animation: animation,
@@ -440,10 +456,10 @@
final double labelPadding;
switch (info.labelBehavior) {
case NavigationDestinationLabelBehavior.alwaysShow:
- labelPadding = 10;
+ labelPadding = 8;
break;
case NavigationDestinationLabelBehavior.onlyShowSelected:
- labelPadding = selected ? 10 : 0;
+ labelPadding = selected ? 8 : 0;
break;
case NavigationDestinationLabelBehavior.alwaysHide:
labelPadding = 0;
diff --git a/packages/flutter/test/material/navigation_bar_test.dart b/packages/flutter/test/material/navigation_bar_test.dart
index 17028c6..cba5d37 100644
--- a/packages/flutter/test/material/navigation_bar_test.dart
+++ b/packages/flutter/test/material/navigation_bar_test.dart
@@ -589,7 +589,7 @@
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
- Offset indicatorCenter = const Offset(600, 30);
+ Offset indicatorCenter = const Offset(600, 32);
const Size includedIndicatorSize = Size(64, 32);
const Size excludedIndicatorSize = Size(74, 40);
@@ -715,7 +715,7 @@
selectedIndex = 1;
await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
await tester.pumpAndSettle();
- indicatorCenter = const Offset(600, 30);
+ indicatorCenter = const Offset(600, 32);
expect(
inkFeatures,
@@ -803,6 +803,96 @@
transform = tester.widget<Transform>(transformFinder).transform;
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 = CircleBorder();
+
+ Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
+ return MaterialApp(
+ theme: theme,
+ home: Scaffold(
+ bottomNavigationBar: NavigationBar(
+ destinations: <Widget>[
+ NavigationDestination(
+ icon: const Icon(Icons.ac_unit),
+ label: 'AC',
+ indicatorColor: indicatorColor,
+ indicatorShape: indicatorShape,
+ ),
+ const NavigationDestination(
+ icon: Icon(Icons.access_alarm),
+ label: 'Alarm',
+ ),
+ ],
+ onDestinationSelected: (int i) { },
+ ),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildNaviagationBar());
+
+ // Test default indicator color and shape.
+ expect(_indicator(tester)?.color, theme.colorScheme.secondaryContainer);
+ expect(_indicator(tester)?.shape, const StadiumBorder());
+
+ await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape));
+
+ // Test custom indicator color and shape.
+ expect(_indicator(tester)?.color, color);
+ expect(_indicator(tester)?.shape, shape);
+ });
+
+ group('Material 2', () {
+ // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
+ // is turned on by default, these tests can be removed.
+
+ 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();
+
+ Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
+ return MaterialApp(
+ theme: theme,
+ home: Scaffold(
+ bottomNavigationBar: NavigationBar(
+ destinations: <Widget>[
+ NavigationDestination(
+ icon: const Icon(Icons.ac_unit),
+ label: 'AC',
+ indicatorColor: indicatorColor,
+ indicatorShape: indicatorShape,
+ ),
+ const NavigationDestination(
+ icon: Icon(Icons.access_alarm),
+ label: 'Alarm',
+ ),
+ ],
+ onDestinationSelected: (int i) { },
+ ),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildNaviagationBar());
+
+ // Test default indicator color and shape.
+ expect(_indicator(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
+ expect(
+ _indicator(tester)?.shape,
+ const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
+ );
+
+ await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape));
+
+ // Test custom indicator color and shape.
+ expect(_indicator(tester)?.color, color);
+ expect(_indicator(tester)?.shape, shape);
+ });
+ });
}
Widget _buildWidget(Widget child) {