Flutter如何无第三方库实现带动画的可展开悬浮操作按钮
以下方案无需依赖第三方插件,通过Flutter原生组件组合即可实现流畅的展开/收起动画:主按钮默认常驻右下角,点击后上方依次弹出多个功能子按钮,伴随位移、透明度、缩放联动过渡,点击空白区域或再次点击主按钮即可收起,和参考效果一致。
完整可运行示例代码:
import 'package:flutter/material.dart'; import 'dart:math' as math; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(primarySwatch: Colors.blue), home: const HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { late final AnimationController _animationController; late final Animation<double> _expandAnimation; bool _isExpanded = false; // 可自定义子按钮的图标、颜色、点击事件 final List<FabItem> _fabItems = [ FabItem(icon: Icons.map, color: Colors.blue, onTap: () => debugPrint("触发地图功能")), FabItem(icon: Icons.add_a_photo, color: Colors.orange, onTap: () => debugPrint("触发拍照功能")), FabItem(icon: Icons.email, color: Colors.green, onTap: () => debugPrint("触发邮件功能")), ]; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 250), reverseDuration: const Duration(milliseconds: 200), ); _expandAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _toggleExpand() { setState(() { _isExpanded = !_isExpanded; _isExpanded ? _animationController.forward() : _animationController.reverse(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("可展开FAB示例")), body: const Center(child: Text("点击右下角悬浮按钮查看展开效果")), floatingActionButton: _buildExpandableFab(), ); } Widget _buildExpandableFab() { return SizedBox.expand( child: Stack( alignment: Alignment.bottomRight, children: [ // 展开状态的透明遮罩,点击空白收起 if (_isExpanded) GestureDetector( onTap: _toggleExpand, behavior: HitTestBehavior.opaque, ), // 动态渲染子按钮 ..._buildExpandingActionButtons(), // 主按钮,带旋转动画 AnimatedBuilder( animation: _expandAnimation, builder: (context, child) { return Transform.rotate( angle: _expandAnimation.value * math.pi / 4, child: FloatingActionButton( onPressed: _toggleExpand, child: const Icon(Icons.add), ), ); }, ) ], ), ); } List<Widget> _buildExpandingActionButtons() { final List<Widget> buttons = []; final int count = _fabItems.length; const double step = 72; // 子按钮垂直间距 for (int i = 0; i < count; i++) { final FabItem item = _fabItems[i]; // 子按钮按顺序延迟触发动画,形成错落弹出效果 final double tweenStart = i / (count + 1); final Animation<double> buttonAnimation = Tween<double>(begin: 0, end: 1).animate( CurvedAnimation( parent: _expandAnimation, curve: Interval(tweenStart, 1.0, curve: Curves.easeOut), ), ); buttons.add( AnimatedBuilder( animation: buttonAnimation, builder: (context, child) { return Positioned( right: 0, bottom: 72 + step * i * buttonAnimation.value, child: Transform.scale( scale: buttonAnimation.value, child: Opacity( opacity: buttonAnimation.value, child: FloatingActionButton( heroTag: "fab_$i", backgroundColor: item.color, mini: true, onPressed: () { _toggleExpand(); item.onTap?.call(); }, child: Icon(item.icon), ), ), ), ); }, ), ); } return buttons; } } // 子按钮数据模型 class FabItem { final IconData icon; final Color color; final VoidCallback? onTap; FabItem({ required this.icon, required this.color, this.onTap, }); }
实现要点说明
- 无第三方依赖:基于
AnimationController、AnimatedBuilder、Stack原生组件实现,动画帧率稳定无卡顿 - 高度可定制:子按钮的图标、颜色、点击回调、尺寸、间距都可自由修改,调整
step参数即可改变子按钮之间的垂直距离 - 动效还原度高:主按钮点击时+号旋转45度变为关闭标识,子按钮按错落时序依次弹出,同时伴随透明度渐变、缩放、上移位移效果
- 交互逻辑完整:展开状态下点击页面任意空白区域即可自动收起,点击子按钮触发对应事件后也会自动收起,避免遮挡页面内容
- 兼容异常场景:为每个子按钮配置唯一
heroTag,解决多FAB共存时的Hero动画冲突问题,不会抛出运行时异常
调整动画速度可直接修改
_animationController的duration参数;如果需要修改子按钮弹出方向(比如向左、向右弹出),仅需调整Positioned的定位参数、位移动画的方向即可。
内容的提问来源于stack exchange,提问作者hemandroid
相关产品推荐
相关产品推荐

