Flutter使用flutter_animator实现TabBar滑动切换同步触发动画的问题
问题核心原因
- 原代码监听
TabController.animation的逐帧回调逻辑错误:滑动切换时动画状态会连续触发多次,且_tabController.index仅在切换完成后才更新,滑动过程中读取到的仍是旧索引,导致前序标签被错误触发动画。 - 未做索引变化的去重判断,单次切换会多次调用动画播放方法。
TabPainter的shouldRepaint返回false,导致自定义指示器无法跟随滑动状态实时重绘。
完整修改方案
修改后的代码已统一兼容点击切换、滑动切换两种场景,动画表现完全一致:
import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_animator/flutter_animator.dart'; class Playground extends StatefulWidget { @override _PlaygroundState createState() => _PlaygroundState(); } class _PlaygroundState extends State<Playground> with TickerProviderStateMixin { late TabController _tabController; late final List<GlobalKey<AnimatorWidgetState>> _animKeys; // 记录上一次选中的索引,避免重复触发动画 late int _lastSelectedIndex; @override void initState() { super.initState(); _lastSelectedIndex = 0; _tabController = TabController(length: 4, vsync: this) // 统一监听Tab切换事件,兼容点击和滑动两种场景 ..addListener(_onTabChanged); _animKeys = List.generate( 4, (index) => GlobalKey<AnimatorWidgetState>(), ); } @override void dispose() { _tabController.dispose(); for (var key in _animKeys) { key.currentState?.dispose(); } super.dispose(); } void _onTabChanged() { // 获取当前目标索引:点击切换时直接取index,滑动切换时取动画值的四舍五入得到即将选中的索引 final currentIndex = _tabController.indexIsChanging ? _tabController.index : _tabController.animation!.value.round(); // 仅索引发生变化时才触发动画 if (currentIndex != _lastSelectedIndex) { _lastSelectedIndex = currentIndex; // 先重置动画状态再播放,避免上次动画未播放完成导致的异常 _animKeys[currentIndex].currentState?.reset(); _animKeys[currentIndex].currentState?.forward(); } } @override Widget build(BuildContext context) { List<Widget> _tabs = List.generate(4, (index) { return ClipRRect( child: ConstrainedBox( constraints: const BoxConstraints( minWidth: 100, maxHeight: 55, ), child: Tada( preferences: const AnimationPreferences( duration: Duration(milliseconds: 1000), autoPlay: AnimationPlayStates.None, ), key: _animKeys[index], child: CustomPaint( painter: TabPainter( animation: _tabController.animation!, index: index, ), child: Tab( child: Text( 'Tab $index', style: const TextStyle(color: Colors.black), ), ), ), ), ), ); }); return Scaffold( appBar: AppBar( backgroundColor: Colors.blue, elevation: 0, title: const Text( 'Tab Bar', style: TextStyle( color: Colors.black, fontWeight: FontWeight.w400, ), ), centerTitle: true, bottom: TabBar( // 无需单独配置onTap,监听已统一处理 controller: _tabController, labelPadding: const EdgeInsets.only(top: 5.0, bottom: 2.0), indicatorColor: Colors.transparent, tabs: _tabs, ), ), body: TabBarView( controller: _tabController, children: List.generate( 4, (index) => FittedBox( child: Text('Tab $index'), )), ), ); } } class TabPainter extends CustomPainter { final Animation<double> animation; final int index; final tabPaint = Paint(); TabPainter({ required this.animation, required this.index, }) : super(repaint: animation); @override void paint(ui.Canvas canvas, ui.Size size) { if ((animation.value - index).abs() < 1) { final rect = Offset.zero & size; canvas.clipRect(rect); canvas.translate(size.width * (animation.value - index), 0); final tabRect = Alignment.bottomCenter.inscribe(Size(size.width, 3), rect); canvas.drawRect(tabRect, tabPaint..color = Colors.black); } } // 配置正确的重绘判断逻辑 @override bool shouldRepaint(covariant TabPainter oldDelegate) => oldDelegate.animation != animation || oldDelegate.index != index; }
优化点说明
- 统一使用
TabController的全局监听处理切换逻辑,无需分开配置点击和滑动事件 - 新增索引变化去重判断,避免重复触发动画
- 动画播放前先重置状态,解决动画未播放完成导致的卡顿异常
- 修复
TabPainter的重绘逻辑,自定义指示器可跟随滑动实时更新
内容的提问来源于stack exchange,提问作者Phil D
相关产品推荐
相关产品推荐

