Flutter底部导航页面切换:实现左右滑入自定义动画
实现Tasks标签页卡片左滑退出动画
我来帮你解决这个问题——要实现切换到Goals标签时Tasks页卡片左滑退出的效果,核心是要在标签切换的时机触发退出动画,并且确保动画能完整播放而不被页面销毁打断。下面是具体的解决方案:
步骤1:改造TasksTab,添加退出动画逻辑
首先我们需要给TasksTab的State添加独立的退出动画控制器,并且提供一个外部可以调用的方法来触发退出动画:
class TasksTab extends StatefulWidget { // 定义GlobalKey,方便外部获取State实例 static final GlobalKey<TasksTabState> tabKey = GlobalKey<TasksTabState>(); const TasksTab({Key? key}) : super(key: key ?? TasksTab.tabKey); @override TasksTabState createState() => TasksTabState(); } class TasksTabState extends State<TasksTab> with TickerProviderStateMixin { late AnimationController _enterController; late AnimationController _exitController; late Animation<double> _enterAnimation; late Animation<double> _exitAnimation; bool _isExiting = false; @override void initState() { super.initState(); // 初始化进入和退出动画控制器 _enterController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); _exitController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); // 进入动画:从屏幕左侧滑入(-1到0) _enterAnimation = Tween<double>(begin: -1.0, end: 0.0).animate(CurvedAnimation( parent: _enterController, curve: Curves.fastOutSlowIn, )); // 退出动画:向屏幕左侧滑出(0到-1) _exitAnimation = Tween<double>(begin: 0.0, end: -1.0).animate(CurvedAnimation( parent: _exitController, curve: Curves.fastOutSlowIn, )); // 页面初始化时触发进入动画 _enterController.forward(); } @override void dispose() { _enterController.dispose(); _exitController.dispose(); super.dispose(); } // 供外部调用的触发退出动画方法 Future<void> startExitAnimation() async { if (!_isExiting) { setState(() => _isExiting = true); await _exitController.forward(); } } Widget todoCard(Todo todo) { final double screenWidth = MediaQuery.of(context).size.width; // 根据当前状态选择使用进入或退出动画 final targetAnimation = _isExiting ? _exitAnimation : _enterAnimation; return AnimatedBuilder( animation: targetAnimation, builder: (context, child) { return Transform( transform: Matrix4.translationValues(targetAnimation.value * screenWidth, 0.0, 0.0), child: child, ); }, child: ExpansionTile( leading: CircularCheckBox( activeColor: Colors.redAccent, value: todo.complete, materialTapTargetSize: MaterialTapTargetSize.padded, onChanged: (value) { print(value); print("Current todo: $todo"); }, ), title: Text( todo.task, style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic, ), ), children: [ Column( children: [Text(todo.note)], ), ], ), ); } // 其余_buildTodosList和build方法保持不变 }
步骤2:改造Home页面,监听标签切换并触发动画
之前的问题是切换标签时TasksTab会被立即销毁,导致动画无法完整播放。我们可以用IndexedStack保持页面状态,或者延迟页面切换的时机,确保动画播放完成:
class Home extends StatefulWidget { final String name; const Home({Key? key, required this.name}) : super(key: key); @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { int _currentPageIndex = 0; final List<AppTab> _tabs = [AppTab.tasks, AppTab.goals, AppTab.account]; // 预加载所有页面,用IndexedStack保持状态 final List<Widget> _pages = [ BlocProvider( create: (context) => TodosBloc(todosRepository: TodosRepository())..add(LoadTodos()), child: const Padding( padding: EdgeInsets.only(top: 64.0, left: 8.0, right: 8.0), child: TasksTab(), ), ), const GoalsTab(), Container(child: Center(child: Text('Account Tab'))), ]; @override Widget build(BuildContext context) { final tabBloc = BlocProvider.of<TabBloc>(context); return BlocListener<TabBloc, AppTab>( bloc: tabBloc, listener: (context, activeTab) { final targetIndex = _tabs.indexOf(activeTab); // 检测是否从Tasks标签切换到Goals标签 if (_currentPageIndex == 0 && targetIndex == 1) { // 获取TasksTab的State并触发退出动画 final tasksTabState = TasksTab.tabKey.currentState; tasksTabState?.startExitAnimation().then((_) { // 动画完成后再切换页面 if (mounted) { setState(() => _currentPageIndex = targetIndex); } }); } else { // 其他标签切换直接执行 setState(() => _currentPageIndex = targetIndex); } }, child: Scaffold( body: IndexedStack( index: _currentPageIndex, children: _pages, ), floatingActionButton: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.add), tooltip: 'Add Todo', ), bottomNavigationBar: TabSelector( activeTab: _tabs[_currentPageIndex], onTabSelected: (tab) => tabBloc.add(UpdateTab(tab)), ), ), ); } }
关键说明
- 分离动画控制器:用两个独立的控制器处理进入和退出动画,避免同一控制器状态冲突,确保动画逻辑清晰。
- GlobalKey跨组件调用:通过GlobalKey可以在Home页面直接调用TasksTabState的方法,触发退出动画。
- 保持页面状态:使用
IndexedStack让所有标签页保持存活,避免切换时页面被立即销毁,确保动画能完整播放。 - 延迟页面切换:在触发退出动画后,等待动画完成再切换到Goals页面,保证用户能看到完整的退出效果。
这样修改后,当你点击Goals标签时,Tasks页的所有卡片会同步执行左滑退出动画,动画完成后才会切换到Goals页面,完美实现你想要的效果。
内容的提问来源于stack exchange,提问作者Raja Babu
相关产品推荐
相关产品推荐

