mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
Added a NavgationBar example with nested Navigators (#98440)
This commit is contained in:
parent
4ee22ed2bd
commit
db4c98fcb0
370
examples/api/lib/material/navigation_bar/navigation_bar.0.dart
Normal file
370
examples/api/lib/material/navigation_bar/navigation_bar.0.dart
Normal file
|
@ -0,0 +1,370 @@
|
|||
// 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';
|
||||
|
||||
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({ Key? key, required this.destination }) : super(key: key);
|
||||
|
||||
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 headline5 = Theme.of(context).textTheme.headline5!;
|
||||
final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
|
||||
primary: destination.color,
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
textStyle: headline5,
|
||||
);
|
||||
|
||||
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: headline5,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Local BottomSheet'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListPage extends StatelessWidget {
|
||||
const ListPage({ Key? key, required this.destination }) : super(key: key);
|
||||
|
||||
final Destination destination;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const int itemCount = 50;
|
||||
final ButtonStyle buttonStyle = OutlinedButton.styleFrom(
|
||||
primary: destination.color,
|
||||
fixedSize: const Size.fromHeight(128),
|
||||
textStyle: Theme.of(context).textTheme.headline5,
|
||||
);
|
||||
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: MaterialStateProperty.all<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({ Key? key, required this.destination }) : super(key: key);
|
||||
|
||||
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.headline4?.copyWith(
|
||||
color: widget.destination.color,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: widget.destination.color,
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DestinationView extends StatefulWidget {
|
||||
const DestinationView({
|
||||
Key? key,
|
||||
required this.destination,
|
||||
required this.navigatorKey,
|
||||
}) : super(key: key);
|
||||
|
||||
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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
const Home({ Key? key }) : super(key: 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: KeyedSubtree(
|
||||
key: GlobalKey(),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(const MaterialApp(home: Home()));
|
||||
}
|
|
@ -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.0.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);
|
||||
});
|
||||
}
|
|
@ -59,6 +59,21 @@ import 'tooltip.dart';
|
|||
/// ),
|
||||
/// ),
|
||||
/// ```
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example has a [NavigationBar] where each destination has its
|
||||
/// own Navigator, Scaffold, and Appbar. That means that each
|
||||
/// destination has an independent route history and (app bar) back
|
||||
/// button. A [Stack] is used to display one destination at a time and
|
||||
/// destination changes are handled by cross fade transitions. Destinations
|
||||
/// that have been completely faded out are [Offstage].
|
||||
///
|
||||
/// One can see that the appearance of each destination's dialogs, bottom sheet,
|
||||
/// list scrolling state, and text field state, persist when another destination
|
||||
/// is selected.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.0.dart **
|
||||
/// {@end-tool}
|
||||
class NavigationBar extends StatelessWidget {
|
||||
/// Creates a Material 3 Navigation Bar component.
|
||||
///
|
||||
|
|
Loading…
Reference in a new issue