一般来说,go_router用于从一个页面导航到另一个页面.我们导航的页面将显示在前一页的前面.但是如何在showModalBottomSheet中实现这一点呢?
我有一个模式,只显示半个屏幕.然后,当我单击导航到新页面的按钮时,该页面显示在模式的前面.但我想要的是新页面仍然显示内半模式.
一般来说,go_router用于从一个页面导航到另一个页面.我们导航的页面将显示在前一页的前面.但是如何在showModalBottomSheet中实现这一点呢?
我有一个模式,只显示半个屏幕.然后,当我单击导航到新页面的按钮时,该页面显示在模式的前面.但我想要的是新页面仍然显示内半模式.
// Copyright 2013 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 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:english_words/english_words.dart';
void main() {
runApp(MusicAppDemo());
}
class MusicAppDemo extends StatelessWidget {
MusicAppDemo({super.key});
final MusicDatabase database = MusicDatabase.mock();
final GoRouter _router = GoRouter(
initialLocation: '/library',
routes: <RouteBase>[
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return MusicAppShell(
currentIndex: switch (state.uri.path) {
var p when p.startsWith('/recents') => 1,
var p when p.startsWith('/search') => 2,
_ => 0,
},
child: child,
);
},
routes: <RouteBase>[
GoRoute(
path: '/library',
pageBuilder: (context, state) {
return FadeTransitionPage(
child: const LibraryScreen(),
key: state.pageKey,
);
},
routes: <RouteBase>[
GoRoute(
path: 'album/:albumId',
builder: (BuildContext context, GoRouterState state) {
return AlbumScreen(
albumId: state.pathParameters['albumId'],
);
},
routes: [
GoRoute(
path: 'song/:songId',
// Display on the root Navigator
builder: (BuildContext context, GoRouterState state) {
return SongScreen(
songId: state.pathParameters['songId']!,
);
},
),
],
),
],
),
GoRoute(
path: '/recents',
pageBuilder: (context, state) {
return FadeTransitionPage(
child: const RecentlyPlayedScreen(),
key: state.pageKey,
);
},
routes: <RouteBase>[
GoRoute(
path: 'song/:songId',
// Display on the root Navigator
builder: (BuildContext context, GoRouterState state) {
return SongScreen(
songId: state.pathParameters['songId']!,
);
},
),
],
),
GoRoute(
path: '/search',
pageBuilder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
return FadeTransitionPage(
child: SearchScreen(
query: query,
),
key: state.pageKey,
);
},
),
],
),
/// This one is outsise ShellRoute so it won't share any state
GoRoute(
path: '/settings',
pageBuilder: (context, state) {
return FadeTransitionPage(
child: const SettingsScreen(),
key: state.pageKey,
);
},
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Music app',
theme: ThemeData(
colorSchemeSeed: Colors.pink,
useMaterial3: true,
),
routerConfig: _router,
builder: (context, child) {
return MusicDatabaseScope(
state: database,
child: child!,
);
},
);
}
}
class MusicAppShell extends StatelessWidget {
final Widget child;
final int currentIndex;
const MusicAppShell({
super.key,
required this.child,
required this.currentIndex,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
/// This button only purpose is open a bottomsheet so you can select and see that works the same as the NavigationBar, but with this your bottomSheet won't be disposed or closed
floatingActionButton: FloatingActionButton(
onPressed: () async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
enableDrag: true,
builder: (context) {
return DraggableScrollableSheet(
maxChildSize: 0.5,
expand: false,
initialChildSize: 0.25,
builder: (context, scrollController) {
final theme = Theme.of(context);
return Material(
color: theme.scaffoldBackgroundColor,
shape: theme.bottomSheetTheme.shape,
clipBehavior: Clip.hardEdge,
child: ListView(
children: [
ListTile(
leading: const Icon(Icons.my_library_music_rounded),
title: const Text('Library'),
onTap: () => _onItemTapped(0, context),
),
ListTile(
leading: const Icon(Icons.timelapse),
title: const Text('Recently Played'),
onTap: () => _onItemTapped(1, context),
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Search'),
onTap: () => _onItemTapped(2, context),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () => _onItemTapped(3, context),
),
],
),
);
},
);
},
);
},
child: const Icon(Icons.more),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.my_library_music_rounded),
label: 'Library',
),
BottomNavigationBarItem(
icon: Icon(Icons.timelapse),
label: 'Recently Played',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
),
],
currentIndex: currentIndex,
onTap: (int idx) => _onItemTapped(idx, context),
),
);
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 1:
GoRouter.of(context).go('/recents');
break;
case 2:
GoRouter.of(context).go('/search');
break;
case 3:
GoRouter.of(context).go('/settings');
break;
case 0:
default:
GoRouter.of(context).go('/library');
break;
}
}
}
/// Part of Dartpad's example but can be ignored
class LibraryScreen extends StatelessWidget {
const LibraryScreen({super.key});
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Library'),
),
body: ListView.builder(
itemBuilder: (context, albumId) {
final album = database.albums[albumId];
return AlbumTile(
album: album,
onTap: () {
GoRouter.of(context).go('/library/album/$albumId');
},
);
},
itemCount: database.albums.length,
),
);
}
}
class RecentlyPlayedScreen extends StatelessWidget {
const RecentlyPlayedScreen({super.key});
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
final songs = database.recentlyPlayed;
return Scaffold(
appBar: AppBar(
title: const Text('Recently Played'),
),
body: ListView.builder(
itemBuilder: (context, index) {
final song = songs[index];
final albumIdInt = int.tryParse(song.albumId)!;
final album = database.albums[albumIdInt];
return SongTile(
album: album,
song: song,
onTap: () {
GoRouter.of(context).go('/recents/song/${song.fullId}');
},
);
},
itemCount: songs.length,
),
);
}
}
class SearchScreen extends StatefulWidget {
final String query;
const SearchScreen({super.key, required this.query});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
String? _currentQuery;
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
final songs = database.search(widget.query);
return Scaffold(
appBar: AppBar(
title: const Text('Search'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
decoration: const InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
),
onChanged: (String? newSearch) {
_currentQuery = newSearch;
},
onEditingComplete: () {
GoRouter.of(context).go(
'/search?q=$_currentQuery',
);
},
),
),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final song = songs[index];
return SongTile(
album: database.albums[int.tryParse(song.albumId)!],
song: song,
onTap: () {
GoRouter.of(context).go(
'/library/album/${song.albumId}/song/${song.fullId}');
},
);
},
itemCount: songs.length,
),
),
],
),
);
}
}
class AlbumScreen extends StatelessWidget {
final String? albumId;
const AlbumScreen({
required this.albumId,
super.key,
});
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
final albumIdInt = int.tryParse(albumId ?? '');
final album = database.albums[albumIdInt!];
return Scaffold(
appBar: AppBar(
title: Text('Album - ${album.title}'),
),
body: Center(
child: Column(
children: [
Row(
children: [
SizedBox(
width: 200,
height: 200,
child: Container(
color: album.color,
margin: const EdgeInsets.all(8),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.title,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
album.artist,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
],
),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final song = album.songs[index];
return ListTile(
title: Text(song.title),
leading: SizedBox(
width: 50,
height: 50,
child: Container(
color: album.color,
margin: const EdgeInsets.all(8),
),
),
trailing: SongDuration(
duration: song.duration,
),
onTap: () {
GoRouter.of(context)
.go('/library/album/$albumId/song/${song.fullId}');
},
);
},
itemCount: album.songs.length,
),
),
],
),
),
);
}
}
class SongScreen extends StatelessWidget {
final String songId;
const SongScreen({
super.key,
required this.songId,
});
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
final song = database.getSongById(songId);
final albumIdInt = int.tryParse(song.albumId);
final album = database.albums[albumIdInt!];
return Scaffold(
appBar: AppBar(
title: Text('Song - ${song.title}'),
),
body: Column(
children: [
Row(
children: [
SizedBox(
width: 300,
height: 300,
child: Container(
color: album.color,
margin: const EdgeInsets.all(8),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.title,
style: Theme.of(context).textTheme.displayMedium,
),
Text(
album.title,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
)
],
)
],
),
);
}
}
class MusicDatabase {
final List<Album> albums;
final List<Song> recentlyPlayed;
final Map<String, Song> _allSongs = {};
MusicDatabase(this.albums, this.recentlyPlayed) {
_populateAllSongs();
}
factory MusicDatabase.mock() {
final albums = _mockAlbums().toList();
final recentlyPlayed = _mockRecentlyPlayed(albums).toList();
return MusicDatabase(albums, recentlyPlayed);
}
Song getSongById(String songId) {
if (_allSongs.containsKey(songId)) {
return _allSongs[songId]!;
}
throw ('No song with ID $songId found.');
}
List<Song> search(String searchString) {
final songs = <Song>[];
for (var song in _allSongs.values) {
final album = albums[int.tryParse(song.albumId)!];
if (song.title.contains(searchString) ||
album.title.contains(searchString)) {
songs.add(song);
}
}
return songs;
}
void _populateAllSongs() {
for (var album in albums) {
for (var song in album.songs) {
_allSongs[song.fullId] = song;
}
}
}
static MusicDatabase of(BuildContext context) {
final routeStateScope =
context.dependOnInheritedWidgetOfExactType<MusicDatabaseScope>();
if (routeStateScope == null) throw ('No RouteState in scope!');
return routeStateScope.state;
}
static Iterable<Album> _mockAlbums() sync* {
for (var i = 0; i < Colors.primaries.length; i++) {
final color = Colors.primaries[i];
final title = WordPair.random().toString();
final artist = WordPair.random().toString();
final songs = <Song>[];
for (var j = 0; j < 12; j++) {
final minutes = math.Random().nextInt(3) + 3;
final seconds = math.Random().nextInt(60);
final title = WordPair.random();
final duration = Duration(minutes: minutes, seconds: seconds);
final song = Song('$j', '$i', '$title', duration);
songs.add(song);
}
yield Album('$i', title, artist, color, songs);
}
}
static Iterable<Song> _mockRecentlyPlayed(List<Album> albums) sync* {
for (var album in albums) {
final songIndex = math.Random().nextInt(album.songs.length);
yield album.songs[songIndex];
}
}
}
class MusicDatabaseScope extends InheritedWidget {
final MusicDatabase state;
const MusicDatabaseScope({
required this.state,
required super.child,
super.key,
});
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return oldWidget is MusicDatabaseScope && state != oldWidget.state;
}
}
class Album {
final String id;
final String title;
final String artist;
final Color color;
final List<Song> songs;
Album(this.id, this.title, this.artist, this.color, this.songs);
}
class Song {
final String id;
final String albumId;
final String title;
final Duration duration;
Song(this.id, this.albumId, this.title, this.duration);
String get fullId => '$albumId-$id';
}
class AlbumTile extends StatelessWidget {
final Album album;
final VoidCallback? onTap;
const AlbumTile({super.key, required this.album, this.onTap});
@override
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox(
width: 50,
height: 50,
child: Container(
color: album.color,
),
),
title: Text(album.title),
subtitle: Text(album.artist),
onTap: onTap,
);
}
}
class SongTile extends StatelessWidget {
final Album album;
final Song song;
final VoidCallback? onTap;
const SongTile(
{super.key, required this.album, required this.song, this.onTap});
@override
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox(
width: 50,
height: 50,
child: Container(
color: album.color,
margin: const EdgeInsets.all(8),
),
),
title: Text(song.title),
trailing: SongDuration(
duration: song.duration,
),
onTap: onTap,
);
}
}
class SongDuration extends StatelessWidget {
final Duration duration;
const SongDuration({
required this.duration,
super.key,
});
@override
Widget build(BuildContext context) {
return Text(
'${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}');
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
body: Center(
child: IconButton(
icon: const Icon(Icons.home),
onPressed: () => GoRouter.of(context).go('/library'),
),
),
);
}
}
/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage<void> {
/// Creates a [FadeTransitionPage].
FadeTransitionPage({
required super.key,
required super.child,
}) : super(
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) =>
FadeTransition(
opacity: animation.drive(_curveTween),
child: child,
));
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
}
此代码摘自GoRoad DartPad示例,但经过修改,以便您可以看到它是如何工作的
Gist个
本例直观地展示了如何使用带有BottomNavigationBar的Shellroute,但我添加了一个打开底板的工厂作为示例,在那里您可以 Select 相同的选项,背景小部件将更改,除了最后一个"设置"之外,我在shell 路径外创建它,以便您可以看到,使用外部的路由将更改整个小部件(处理底板)