如何在Flutter中实现带动画指示器的垂直Windows风格TabBar菜单
Flutter 带动画滑动指示器的顶部菜单实现
实现核心逻辑
- 用
GlobalKey标记每个菜单项,动态获取选中项的宽度与水平位置 - 通过
AnimationController实现指示器的平滑位移动画 - 自定义
CustomPainter同时绘制底部分割线与悬浮在分割线上的指示器,保证层级正确
完整可运行示例代码
import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('动画菜单示例')), body: const AnimatedTabMenu(), ), ); } } class AnimatedTabMenu extends StatefulWidget { const AnimatedTabMenu({super.key}); @override State<AnimatedTabMenu> createState() => _AnimatedTabMenuState(); } class _AnimatedTabMenuState extends State<AnimatedTabMenu> with SingleTickerProviderStateMixin { int _selectedIndex = 0; late AnimationController _animationController; final List<GlobalKey> _tabKeys = List.generate(4, (index) => GlobalKey()); final List<String> _tabs = ['首页', '分类', '推荐', '我的']; double _indicatorX = 0; double _indicatorWidth = 0; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 250), ); WidgetsBinding.instance.addPostFrameCallback((_) { _updateIndicatorPosition(animate: false); }); } void _updateIndicatorPosition({required bool animate}) { final renderBox = _tabKeys[_selectedIndex].currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return; final targetX = renderBox.localToGlobal(Offset.zero).dx; final targetWidth = renderBox.size.width; if (animate) { final oldX = _indicatorX; final oldWidth = _indicatorWidth; final anim = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut); _animationController.reset(); _animationController.addListener(() { setState(() { _indicatorX = oldX + (targetX - oldX) * anim.value; _indicatorWidth = oldWidth + (targetWidth - oldWidth) * anim.value; }); }); _animationController.forward(); } else { setState(() { _indicatorX = targetX; _indicatorWidth = targetWidth; }); } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( children: [ // 菜单项区域 Row( children: List.generate(_tabs.length, (index) { return Expanded( key: _tabKeys[index], child: GestureDetector( onTap: () { if (_selectedIndex == index) return; setState(() { _selectedIndex = index; }); _updateIndicatorPosition(animate: true); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( _tabs[index], textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: _selectedIndex == index ? FontWeight.w600 : FontWeight.normal, color: _selectedIndex == index ? Colors.blue : Colors.grey[700], ), ), ), ), ); }), ), // 分割线+指示器区域 CustomPaint( painter: IndicatorPainter( indicatorX: _indicatorX, indicatorWidth: _indicatorWidth, dividerColor: Colors.grey[300]!, indicatorColor: Colors.blue, dividerHeight: 1, indicatorHeight: 3, ), child: const SizedBox(height: 3, width: double.infinity), ), ], ); } } class IndicatorPainter extends CustomPainter { final double indicatorX; final double indicatorWidth; final Color dividerColor; final Color indicatorColor; final double dividerHeight; final double indicatorHeight; IndicatorPainter({ required this.indicatorX, required this.indicatorWidth, required this.dividerColor, required this.indicatorColor, required this.dividerHeight, required this.indicatorHeight, }); @override void paint(Canvas canvas, Size size) { // 画底部分割线 final dividerPaint = Paint()..color = dividerColor; canvas.drawRect( Rect.fromLTWH(0, size.height - dividerHeight, size.width, dividerHeight), dividerPaint, ); // 画悬浮指示器 if (indicatorWidth > 0) { final indicatorPaint = Paint()..color = indicatorColor; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH( indicatorX, size.height - indicatorHeight, indicatorWidth, indicatorHeight, ), const Radius.circular(2), ), indicatorPaint, ); } } @override bool shouldRepaint(covariant IndicatorPainter oldDelegate) { return oldDelegate.indicatorX != indicatorX || oldDelegate.indicatorWidth != indicatorWidth; } }
自定义调整说明
- 如需修改菜单项内容,直接修改
_tabs数组的文本,也可以替换成图标+文本的组合组件 - 如需调整动画时长,修改
AnimationController的duration参数即可 - 如需修改分割线、指示器的颜色与尺寸,调整
IndicatorPainter对应的传参即可 - 如需修改指示器圆角,修改
IndicatorPainter中RRect.fromRectAndRadius的radius参数即可
内容的提问来源于stack exchange,提问作者Steeven Delucis
相关产品推荐
相关产品推荐

