Flutter中如何为ShellRoute自定义路由切换动画(如SlideTransition)
解决GoRouter ShellRoute嵌套路由切换动画不自然问题
问题背景
使用GoRouter 13.0.0的ShellRoute结合BottomNavigationBar实现嵌套路由时,遇到以下问题:
- 路由切换动画生硬不自然
- 尝试为ShellRoute添加SlideTransition动画未生效
- 期望点击BottomNavigationBar右侧项时显示右滑进入动画,左侧项时显示左滑进入动画
- Android与iOS默认路由动画存在差异
问题原因
你之前尝试在ShellRoute的builder中添加动画,但ShellRoute本身作为容器,其内部子路由的切换动画才是需要自定义的核心。直接修改ShellRoute的builder无法影响内部页面的过渡效果,因为内部导航器的页面过渡由子路由的pageBuilder控制,而非Shell容器。
解决方案
1. 记录路由切换方向
在BottomNavigationBar的点击事件中,计算当前路由索引与目标索引的差值,判断切换方向(左滑/右滑),并通过路由的extra参数传递方向信息。
2. 自定义子路由的页面过渡
将ShellRoute内的GoRoute从builder改为pageBuilder,在pageBuilder中根据传递的方向参数,自定义SlideTransition动画,同时可兼容平台默认差异。
修改后的代码
routes.dart
import 'package:baro_project/screens/calendar_screen.dart'; import 'package:baro_project/screens/info_screen.dart'; import 'package:baro_project/screens/youtube_screen.dart'; import 'package:baro_project/widgets/nav_bar.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'screens/login_screen.dart'; import 'screens/main_screen.dart'; import 'screens/noti_screen.dart'; final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Root'); final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Shell'); // 定义切换方向枚举 enum SlideDirection { left, right } final router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/login', routes: <RouteBase>[ GoRoute( path: '/login', builder: (context, state) => const Login(), ), GoRoute( path: '/notification', builder: (context, state) => const NotificationManager(), ), ShellRoute( navigatorKey: _shellNavigatorKey, builder: (context, state, child) => NavBarWidget(child: child), routes: <RouteBase>[ GoRoute( path: '/main', pageBuilder: (context, state) => _buildSlideTransitionPage( context, state, const MainScreen(), state.extra as SlideDirection?, ), ), GoRoute( path: '/calendar', pageBuilder: (context, state) => _buildSlideTransitionPage( context, state, const CalendarScreen(), state.extra as SlideDirection?, ), ), GoRoute( path: '/youtube', pageBuilder: (context, state) => _buildSlideTransitionPage( context, state, const YoutubeScreen(), state.extra as SlideDirection?, ), ), GoRoute( path: '/information', pageBuilder: (context, state) => _buildSlideTransitionPage( context, state, const InformationScreen(), state.extra as SlideDirection?, ), ), ], ), ], ); // 自定义滑动过渡页面 CustomTransitionPage _buildSlideTransitionPage( BuildContext context, GoRouterState state, Widget child, SlideDirection? direction, ) { // 默认方向:如果没有传递方向,根据平台设置默认动画 final beginOffset = switch (direction) { SlideDirection.left => const Offset(-1.0, 0.0), SlideDirection.right => const Offset(1.0, 0.0), _ => Theme.of(context).platform == TargetPlatform.iOS ? const Offset(1.0, 0.0) : const Offset(0.0, 0.3), }; return CustomTransitionPage( key: state.pageKey, child: child, transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween<Offset>( begin: beginOffset, end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeInOut, )), child: child, ); }, ); }
nav_bar.dart
import 'package:baro_project/widgets/app_bar.dart'; import 'package:baro_project/widgets/custom_drawer.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:baro_project/routes.dart'; // 引入定义的SlideDirection class NavBarWidget extends StatelessWidget { const NavBarWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) { return Scaffold( appBar: customAppBar(context), body: child, bottomNavigationBar: BottomNavigationBar( unselectedItemColor: Colors.grey, selectedItemColor: Colors.black, type: BottomNavigationBarType.fixed, items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.house, size: 15.0), label: 'Main', ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.calendar, size: 15.0), label: 'Calendar', ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.youtube, size: 15.0), label: 'Youtube', ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.user, size: 15.0), label: 'Information', ), ], onTap: (index) => _onItemTapped(index, context), currentIndex: _calculateIndex(context), ), endDrawer: customDrawer(context), ); } void _onItemTapped(int index, BuildContext context) { final currentIndex = _calculateIndex(context); SlideDirection? direction; // 判断切换方向:目标索引大于当前索引则右滑进入,反之左滑进入 if (index > currentIndex) { direction = SlideDirection.right; } else if (index < currentIndex) { direction = SlideDirection.left; } switch (index) { case 0: GoRouter.of(context).go('/main', extra: direction); break; case 1: GoRouter.of(context).go('/calendar', extra: direction); break; case 2: GoRouter.of(context).go('/youtube', extra: direction); break; case 3: GoRouter.of(context).go('/information', extra: direction); break; } } int _calculateIndex(BuildContext context) { final currentLocation = GoRouterState.of(context).uri.toString(); if (currentLocation == '/main') { return 0; } if (currentLocation == '/calendar') { return 1; } if (currentLocation == '/youtube') { return 2; } if (currentLocation == '/information') { return 3; } return 0; } }
说明
- 通过
CustomTransitionPage替代默认页面,完全控制过渡动画 - 在BottomNavigationBar点击时计算方向,通过
extra参数传递给路由 - 动画方向逻辑:点击右侧项(索引更大)时,新页面从右侧滑入;点击左侧项(索引更小)时,新页面从左侧滑入
- 兼容平台差异:如果未传递方向(如首次进入),根据平台使用默认偏移量
内容的提问来源于stack exchange,提问作者IHLee
相关产品推荐
相关产品推荐

