You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 23:54:57