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 ac22535fb317..41cca1b27597 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 42d21aa50ee1..06191a794282 100644 --- a/examples/api/lib/material/navigation_bar/navigation_bar.1.dart +++ b/examples/api/lib/material/navigation_bar/navigation_bar.1.dart @@ -2,366 +2,97 @@ // 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())); -} - -class Home extends StatefulWidget { - const Home({ super.key }); - - @override - State createState() => _HomeState(); -} - -class _HomeState extends State with TickerProviderStateMixin { - static const List allDestinations = [ - 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), - ]; +void main() => runApp(const NavigationBarApp()); - late final List> navigatorKeys; - late final List destinationKeys; - late final List destinationFaders; - late final List 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>.generate(allDestinations.length, (int index) => GlobalKey()).toList(); - destinationFaders = List.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 RootPage extends StatelessWidget { - const RootPage({ super.key, required this.destination }); +class NavigationExample extends StatefulWidget { + const NavigationExample({super.key}); - final Destination destination; + @override + State createState() => _NavigationExampleState(); +} - Widget _buildDialog(BuildContext context) { - return AlertDialog( - title: Text('${destination.title} AlertDialog'), - actions: [ - TextButton( - onPressed: () { Navigator.pop(context); }, - child: const Text('OK'), - ), - ], - ); - } +class _NavigationExampleState extends State { + 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 [ + 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: [ - 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: [ + 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'), - ); - }, - ), - ], - ), - ), - ); - } -} - -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.lerp(destination.color[100], Colors.white, index / itemCount)! - ), + child: const Text('alwaysShow'), ), - 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 createState() => _TextPageState(); -} - -class _TextPageState extends State { - 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, - ), + ElevatedButton( + onPressed: () { + setState(() { + labelBehavior = NavigationDestinationLabelBehavior.onlyShowSelected; + }); + }, + child: const Text('onlyShowSelected'), + ), + ElevatedButton( + onPressed: () { + setState(() { + labelBehavior = NavigationDestinationLabelBehavior.alwaysHide; + }); + }, + child: const Text('alwaysHide'), + ), + ], ), - ), + ], ), ), ); } } - -class DestinationView extends StatefulWidget { - const DestinationView({ - super.key, - required this.destination, - required this.navigatorKey, - }); - - final Destination destination; - final Key navigatorKey; - - @override - State createState() => _DestinationViewState(); -} - -class _DestinationViewState extends State { - @override - Widget build(BuildContext context) { - return Navigator( - key: widget.navigatorKey, - onGenerateRoute: (RouteSettings settings) { - return MaterialPageRoute( - 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 000000000000..42d21aa50ee1 --- /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 createState() => _HomeState(); +} + +class _HomeState extends State with TickerProviderStateMixin { + static const List allDestinations = [ + 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> navigatorKeys; + late final List destinationKeys; + late final List destinationFaders; + late final List 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>.generate(allDestinations.length, (int index) => GlobalKey()).toList(); + destinationFaders = List.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: [ + 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: [ + 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.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 createState() => _TextPageState(); +} + +class _TextPageState extends State { + 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 createState() => _DestinationViewState(); +} + +class _DestinationViewState extends State { + @override + Widget build(BuildContext context) { + return Navigator( + key: widget.navigatorKey, + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + 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 501058eb21e9..b78688985bdf 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 @@ void main() { 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 23c35f9c0fe2..13005652c05e 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); - - 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); - }); + expect(find.text('Label behavior: alwaysShow'), findsOneWidget); + expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.alwaysShow); - - 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')); + /// Test onlyShowSelected label behavior button. + await tester.tap(find.widgetWithText(ElevatedButton, 'onlyShowSelected')); 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); + expect(find.text('Label behavior: onlyShowSelected'), findsOneWidget); + navigationBarWidget = tester.firstWidget(find.byType(NavigationBar)); + expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.onlyShowSelected); - await tester.tap(find.byType(BackButton)); + /// Test alwaysHide label behavior button. + await tester.tap(find.widgetWithText(ElevatedButton, 'alwaysHide')); 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 000000000000..cba73818e3cd --- /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 0b24712426df..b17704bdfd4b 100644 --- a/packages/flutter/lib/src/material/navigation_bar.dart +++ b/packages/flutter/lib/src/material/navigation_bar.dart @@ -53,6 +53,14 @@ const double _kIndicatorWidth = 64; /// {@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 @@ const double _kIndicatorWidth = 64; /// 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 @@ class NavigationDestination extends StatelessWidget { super.key, required this.icon, this.selectedIcon, + this.indicatorColor, + this.indicatorShape, required this.label, this.tooltip, }); @@ -290,6 +300,12 @@ 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]. /// @@ -335,8 +351,8 @@ class NavigationDestination extends StatelessWidget { children: [ 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 @@ class _NavigationDestinationBuilder extends StatelessWidget { 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 17028c66a23d..cba5d376f312 100644 --- a/packages/flutter/test/material/navigation_bar_test.dart +++ b/packages/flutter/test/material/navigation_bar_test.dart @@ -589,7 +589,7 @@ void main() { 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 @@ void main() { 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 @@ void main() { transform = tester.widget(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: [ + 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: [ + 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) {