Unverified Commit f07db401 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

`NavigationBar` improvements (#116992)

parent f0ea3764
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() => runApp(const ExampleApp()); void main() => runApp(const NavigationBarApp());
class ExampleApp extends StatelessWidget { class NavigationBarApp extends StatelessWidget {
const ExampleApp({super.key}); const NavigationBarApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
......
...@@ -2,366 +2,97 @@ ...@@ -2,366 +2,97 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/// Flutter code sample for [NavigationBar] with nested [Navigator] destinations. /// Flutter code sample for [NavigationBar].
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() => runApp(const NavigationBarApp());
runApp(const MaterialApp(home: Home()));
}
class Home extends StatefulWidget { class NavigationBarApp extends StatelessWidget {
const Home({ super.key }); const NavigationBarApp({super.key});
@override @override
State<Home> createState() => _HomeState(); Widget build(BuildContext context) {
return const MaterialApp(home: NavigationExample());
}
} }
class _HomeState extends State<Home> with TickerProviderStateMixin<Home> { class NavigationExample extends StatefulWidget {
static const List<Destination> allDestinations = <Destination>[ const NavigationExample({super.key});
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 @override
void initState() { State<NavigationExample> createState() => _NavigationExampleState();
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 class _NavigationExampleState extends State<NavigationExample> {
void dispose() { int currentPageIndex = 0;
for (final AnimationController controller in destinationFaders) { NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
controller.dispose();
}
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return Scaffold(
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( bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex, labelBehavior: labelBehavior,
selectedIndex: currentPageIndex,
onDestinationSelected: (int index) { onDestinationSelected: (int index) {
setState(() { setState(() {
selectedIndex = index; currentPageIndex = index;
}); });
}, },
destinations: allDestinations.map((Destination destination) { destinations: const <Widget>[
return NavigationDestination( NavigationDestination(
icon: Icon(destination.icon, color: destination.color), icon: Icon(Icons.explore),
label: destination.title, label: 'Explore',
);
}).toList(),
), ),
NavigationDestination(
icon: Icon(Icons.commute),
label: 'Commute',
), ),
); NavigationDestination(
} selectedIcon: Icon(Icons.bookmark),
} icon: Icon(Icons.bookmark_border),
label: 'Saved',
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( body: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Label behavior: ${labelBehavior.name}'),
const SizedBox(height: 10),
OverflowBar(
spacing: 10.0,
children: <Widget>[ children: <Widget>[
ElevatedButton( ElevatedButton(
style: buttonStyle,
onPressed: () { onPressed: () {
Navigator.pushNamed(context, '/list'); setState(() {
labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
});
}, },
child: const Text('Push /list'), child: const Text('alwaysShow'),
), ),
const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
style: buttonStyle,
onPressed: () { onPressed: () {
showDialog( setState(() {
context: context, labelBehavior = NavigationDestinationLabelBehavior.onlyShowSelected;
useRootNavigator: false, });
builder: _buildDialog,
);
}, },
child: const Text('Local Dialog'), child: const Text('onlyShowSelected'),
), ),
const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
style: buttonStyle,
onPressed: () { onPressed: () {
showDialog( setState(() {
context: context, labelBehavior = NavigationDestinationLabelBehavior.alwaysHide;
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'),
);
}, },
child: const Text('alwaysHide'),
), ),
], ],
), ),
), ],
);
}
}
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();
},
);
},
);
}
}
// 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();
},
);
},
);
}
}
...@@ -11,10 +11,9 @@ void main() { ...@@ -11,10 +11,9 @@ void main() {
testWidgets('Navigation bar updates destination on tap', testWidgets('Navigation bar updates destination on tap',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const example.ExampleApp(), const example.NavigationBarApp(),
); );
final NavigationBar navigationBarWidget = final NavigationBar navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
tester.firstWidget(find.byType(NavigationBar));
/// NavigationDestinations must be rendered /// NavigationDestinations must be rendered
expect(find.text('Explore'), findsOneWidget); expect(find.text('Explore'), findsOneWidget);
......
...@@ -3,106 +3,41 @@ ...@@ -3,106 +3,41 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; 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'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('RootPage: only selected destination is on stage', (WidgetTester tester) async { testWidgets('Navigation bar updates label behavior when tapping buttons',
await tester.pumpWidget(const MaterialApp(home: example.Home())); (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavigationBarApp(),
);
NavigationBar navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
const String tealTitle = 'Teal RootPage - /'; expect(find.text('Label behavior: alwaysShow'), findsOneWidget);
const String cyanTitle = 'Cyan RootPage - /';
const String orangeTitle = 'Orange RootPage - /';
const String blueTitle = 'Blue RootPage - /';
await tester.tap(find.widgetWithText(NavigationDestination, 'Teal')); /// Test alwaysShow label behavior button.
await tester.tap(find.widgetWithText(ElevatedButton, 'alwaysShow'));
await tester.pumpAndSettle(); 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')); expect(find.text('Label behavior: alwaysShow'), findsOneWidget);
await tester.pumpAndSettle(); expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.alwaysShow);
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);
});
/// Test onlyShowSelected label behavior button.
testWidgets('ListPage', (WidgetTester tester) async { await tester.tap(find.widgetWithText(ElevatedButton, 'onlyShowSelected'));
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(); await tester.pumpAndSettle();
expect(find.text('Teal ListPage - /list'), findsOneWidget);
expect(find.text('Push /text [0]'), findsOneWidget);
await tester.tap(find.widgetWithText(NavigationDestination, 'Orange')); expect(find.text('Label behavior: onlyShowSelected'), findsOneWidget);
await tester.pumpAndSettle(); navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
await tester.tap(find.widgetWithText(ElevatedButton, 'Push /list')); expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.onlyShowSelected);
await tester.pumpAndSettle();
expect(find.text('Orange ListPage - /list'), findsOneWidget);
expect(find.text('Push /text [0]'), findsOneWidget);
await tester.tap(find.byType(BackButton)); /// Test alwaysHide label behavior button.
await tester.tap(find.widgetWithText(ElevatedButton, 'alwaysHide'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Orange RootPage - /'), findsOneWidget);
await tester.tap(find.widgetWithText(NavigationDestination, 'Teal')); expect(find.text('Label behavior: alwaysHide'), findsOneWidget);
await tester.pumpAndSettle(); navigationBarWidget = tester.firstWidget(find.byType(NavigationBar));
expect(find.text('Teal ListPage - /list'), findsOneWidget); expect(navigationBarWidget.labelBehavior, NavigationDestinationLabelBehavior.alwaysHide);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Teal RootPage - /'), findsOneWidget);
}); });
} }
// 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);
});
}
...@@ -53,6 +53,14 @@ const double _kIndicatorWidth = 64; ...@@ -53,6 +53,14 @@ const double _kIndicatorWidth = 64;
/// {@end-tool} /// {@end-tool}
/// ///
/// {@tool dartpad} /// {@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] /// This example shows a [NavigationBar] as it is used within a [Scaffold]
/// widget when there are nested navigators that provide local navigation. The /// widget when there are nested navigators that provide local navigation. The
/// [NavigationBar] has four [NavigationDestination] widgets with different /// [NavigationBar] has four [NavigationDestination] widgets with different
...@@ -60,7 +68,7 @@ const double _kIndicatorWidth = 64; ...@@ -60,7 +68,7 @@ const double _kIndicatorWidth = 64;
/// item's index and displays a corresponding page with its own local navigator /// item's index and displays a corresponding page with its own local navigator
/// in the body of a [Scaffold]. /// 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} /// {@end-tool}
/// See also: /// See also:
/// ///
...@@ -266,6 +274,8 @@ class NavigationDestination extends StatelessWidget { ...@@ -266,6 +274,8 @@ class NavigationDestination extends StatelessWidget {
super.key, super.key,
required this.icon, required this.icon,
this.selectedIcon, this.selectedIcon,
this.indicatorColor,
this.indicatorShape,
required this.label, required this.label,
this.tooltip, this.tooltip,
}); });
...@@ -290,6 +300,12 @@ class NavigationDestination extends StatelessWidget { ...@@ -290,6 +300,12 @@ class NavigationDestination extends StatelessWidget {
/// would use a size of 24.0 and [ColorScheme.onSurface]. /// would use a size of 24.0 and [ColorScheme.onSurface].
final Widget? selectedIcon; 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 /// The text label that appears below the icon of this
/// [NavigationDestination]. /// [NavigationDestination].
/// ///
...@@ -335,8 +351,8 @@ class NavigationDestination extends StatelessWidget { ...@@ -335,8 +351,8 @@ class NavigationDestination extends StatelessWidget {
children: <Widget>[ children: <Widget>[
NavigationIndicator( NavigationIndicator(
animation: animation, animation: animation,
color: navigationBarTheme.indicatorColor ?? defaults.indicatorColor!, color: indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
shape: navigationBarTheme.indicatorShape ?? defaults.indicatorShape! shape: indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
), ),
_StatusTransitionWidgetBuilder( _StatusTransitionWidgetBuilder(
animation: animation, animation: animation,
...@@ -440,10 +456,10 @@ class _NavigationDestinationBuilder extends StatelessWidget { ...@@ -440,10 +456,10 @@ class _NavigationDestinationBuilder extends StatelessWidget {
final double labelPadding; final double labelPadding;
switch (info.labelBehavior) { switch (info.labelBehavior) {
case NavigationDestinationLabelBehavior.alwaysShow: case NavigationDestinationLabelBehavior.alwaysShow:
labelPadding = 10; labelPadding = 8;
break; break;
case NavigationDestinationLabelBehavior.onlyShowSelected: case NavigationDestinationLabelBehavior.onlyShowSelected:
labelPadding = selected ? 10 : 0; labelPadding = selected ? 8 : 0;
break; break;
case NavigationDestinationLabelBehavior.alwaysHide: case NavigationDestinationLabelBehavior.alwaysHide:
labelPadding = 0; labelPadding = 0;
......
...@@ -589,7 +589,7 @@ void main() { ...@@ -589,7 +589,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); 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 includedIndicatorSize = Size(64, 32);
const Size excludedIndicatorSize = Size(74, 40); const Size excludedIndicatorSize = Size(74, 40);
...@@ -715,7 +715,7 @@ void main() { ...@@ -715,7 +715,7 @@ void main() {
selectedIndex = 1; selectedIndex = 1;
await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected)); await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
indicatorCenter = const Offset(600, 30); indicatorCenter = const Offset(600, 32);
expect( expect(
inkFeatures, inkFeatures,
...@@ -803,6 +803,96 @@ void main() { ...@@ -803,6 +803,96 @@ void main() {
transform = tester.widget<Transform>(transformFinder).transform; transform = tester.widget<Transform>(transformFinder).transform;
expect(transform.getColumn(0)[0], 1.0); 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) { Widget _buildWidget(Widget child) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment