Flutter自定义悬浮按钮设计优化求助
Flutter 悬浮按钮展开面板间距消除优化方案
我想实现一款如图的悬浮按钮展开效果:
目前完成的效果如下:
现在遇到的问题是FAB和展开面板之间存在间距,无法消除,附上我的代码,求优化建议:
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { int _counter = 0; bool _isCrossVisible = false; late AnimationController _animationController; late Animation<double> _rotationAnimation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); // _rotationAnimation = Tween<double>( // begin: 0, // end: 0.5, // ).animate(_animationController); } void _toggleAnimation() { Future.delayed(Duration(milliseconds: 300), () { setState(() { showWidgets = true; }); }); setState(() { if (showWidgets) { showWidgets = false; } _isCrossVisible = !_isCrossVisible; if (_isCrossVisible) { _animationController.forward(); } else { _animationController.reverse(); } }); } @override void dispose() { _animationController.dispose(); super.dispose(); } int _cIndex = 0; void _incrementTab(index) { setState(() { _cIndex = index; }); } bool showWidgets = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), bottomNavigationBar: Container( height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(40), topLeft: Radius.circular(40)), boxShadow: [ BoxShadow(color: Colors.black38, spreadRadius: 0, blurRadius: 10), ], ), child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0), ), child: BottomNavigationBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem(icon: Icon(Icons.favorite), label: ""), BottomNavigationBarItem(icon: Icon(Icons.favorite), label: "") ], ), )), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: Positioned( bottom: 80, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _isCrossVisible ? 200.0 : 0.0, height: _isCrossVisible ? 200.0 : 0.0, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(28.0), ), child: Container( margin: EdgeInsets.only(left: 15, right: 15), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ showWidgets ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Mood chech-in", style: TextStyle(color: Colors.white), ), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.ac_unit, color: Colors.white)), ), ], ) : Container(), showWidgets ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Voice note", style: TextStyle(color: Colors.white)), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.waves_sharp, color: Colors.white)), ), ], ) : Container(), showWidgets ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Add photo", style: TextStyle(color: Colors.white)), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.photo, color: Colors.white)), ), ], ) : Container(), ], ), ), ), Container( margin: EdgeInsets.only(bottom: 25), height: 70, width: 70, child: IconButton( icon: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.rotate( angle: _animationController.value * 0.5 * 3.1415, child: child, ); }, child: Icon( _isCrossVisible ? Icons.close : Icons.add, size: 30, color: Colors.white, ), ), onPressed: _toggleAnimation, ), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(28.0), ), ), ], ), ), ); } }
优化方案
1. 核心问题解决:消除间距与完美衔接
- 移除FAB容器的
margin: EdgeInsets.only(bottom: 25),这是导致间距的直接原因。 - 动态调整面板和FAB的圆角:展开时面板底部圆角设为0,FAB顶部圆角设为0,让两者边缘完全贴合;收起时恢复全圆角。
2. 简化状态管理
删除冗余的showWidgets变量和延迟逻辑,直接用_isCrossVisible控制子项显示,避免状态不同步。
3. 动画与细节优化
用Tween简化旋转动画实现,修正拼写错误,替换Container+margin为Padding更符合Flutter最佳实践。
修改后的完整代码
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { int _counter = 0; bool _isCrossVisible = false; late AnimationController _animationController; late Animation<double> _rotationAnimation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); // 简化旋转动画 _rotationAnimation = Tween<double>(begin: 0, end: 0.5).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } void _toggleAnimation() { setState(() { _isCrossVisible = !_isCrossVisible; _isCrossVisible ? _animationController.forward() : _animationController.reverse(); }); } @override void dispose() { _animationController.dispose(); super.dispose(); } int _cIndex = 0; void _incrementTab(index) { setState(() { _cIndex = index; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), bottomNavigationBar: Container( height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(40), topLeft: Radius.circular(40)), boxShadow: [ BoxShadow(color: Colors.black38, spreadRadius: 0, blurRadius: 10), ], ), child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0), ), child: BottomNavigationBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem(icon: Icon(Icons.favorite), label: ""), BottomNavigationBarItem(icon: Icon(Icons.favorite), label: "") ], ), )), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: Positioned( bottom: 80, child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ // 展开面板 AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _isCrossVisible ? 200.0 : 0.0, height: _isCrossVisible ? 200.0 : 0.0, // 动态调整圆角 decoration: BoxDecoration( color: Colors.blue, borderRadius: _isCrossVisible ? BorderRadius.vertical( top: Radius.circular(28), bottom: Radius.circular(0), ) : BorderRadius.circular(28.0), ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Mood check-in", style: TextStyle(color: Colors.white), ), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.ac_unit, color: Colors.white)), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Voice note", style: TextStyle(color: Colors.white)), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.waves_sharp, color: Colors.white)), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Add photo", style: TextStyle(color: Colors.white)), Container( height: 40, width: 40, decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.5), borderRadius: BorderRadius.circular(15.0), ), child: Center( child: Icon(Icons.photo, color: Colors.white)), ), ], ), ], ), ), ), // FAB按钮 Container( height: 70, width: 70, child: IconButton( icon: AnimatedBuilder( animation: _rotationAnimation, builder: (context, child) { return Transform.rotate( angle: _rotationAnimation.value * 2 * 3.14159, child: child, ); }, child: Icon( _isCrossVisible ? Icons.close : Icons.add, size: 30, color: Colors.white, ), ), onPressed: _toggleAnimation, ), // 动态调整圆角 decoration: BoxDecoration( color: Colors.blue, borderRadius: _isCrossVisible ? BorderRadius.vertical( top: Radius.circular(0), bottom: Radius.circular(28), ) : BorderRadius.circular(28.0), ), ), ], ), ), ); } }
内容的提问来源于stack exchange,提问作者Naqeeb Abid
相关产品推荐
相关产品推荐

