Created
February 16, 2022 11:45
-
-
Save mono0926/fbb136251921d2c2cc96b4a36503321c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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<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, | |
// 細かいケアするため、IndexedStackなどは使わずStackで自前実装 | |
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(); | |
// Widgetツリー安定させるためにOffstageをfalseで使ってる? | |
// (ここでは大して意味ない気がする🤔) | |
return Offstage(child: view, offstage: false); | |
} else { | |
// 非選択タブはフェードアウト | |
destinationFaders[index].reverse(); | |
// フェードアニメーション中はそれが見えるように操作不能にするだけ | |
if (destinationFaders[index].isAnimating) { | |
return IgnorePointer(child: view); | |
} | |
// フェードアニメーション終了している場合はOffstageで隠す | |
// (さらにTickerModeでアニメーション全般止めて無駄な処理抑制するとベター) | |
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())); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment