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

如何在Flutter中实现FAB转换为material.io风格的动作菜单?

实现Material Design风格的FAB转动作菜单效果

嘿,我刚好之前折腾过这个需求!Material.io上那个FAB展开成动作菜单的效果确实很实用,官方Flutter API里确实没有直接的组件,但咱们有两种靠谱的实现方式:

一、用第三方包快速实现

最省心的方式是用成熟的第三方包,比如flutter_speed_dial,这个包完全贴合Material Design规范,支持各种动画效果、自定义样式,上手超快。

基本步骤:

  1. 在pubspec.yaml里添加依赖:
dependencies:
  flutter_speed_dial: ^latest_version
  1. 简单示例代码:
SpeedDial(
  // 主FAB的样式
  icon: Icons.add,
  activeIcon: Icons.close,
  backgroundColor: Colors.blue,
  // 展开后的动作按钮列表
  children: [
    SpeedDialChild(
      child: Icons.camera,
      label: '拍照',
      onTap: () => print('点击拍照'),
    ),
    SpeedDialChild(
      child: Icons.note_add,
      label: '新建笔记',
      onTap: () => print('新建笔记'),
    ),
    // 可以添加更多按钮
  ],
  // 可选:设置动画类型,比如从FAB弹出的效果
  animationCurve: Curves.easeInOut,
  animationDuration: const Duration(milliseconds: 300),
)

这个包还支持自定义label样式、按钮排列方向(垂直/水平),甚至可以添加背景遮罩,完全能满足Material Design的效果要求。

二、自定义实现(适合追求完全可控的场景)

如果你不想依赖第三方包,自己写也不难,核心是用Flutter的动画组件来控制动作按钮的展开/收起状态。

核心思路:

  • 用StatefulWidget维护一个isExpanded状态,标记菜单是否展开
  • 用AnimatedContainer、AnimatedPositioned或者SlideTransition来实现动作按钮的动画
  • 主FAB点击时切换状态,触发动画

简单自定义示例:

class FabActionMenu extends StatefulWidget {
  const FabActionMenu({super.key});

  @override
  State<FabActionMenu> createState() => _FabActionMenuState();
}

class _FabActionMenuState extends State<FabActionMenu> with SingleTickerProviderStateMixin {
  bool _isExpanded = false;
  late AnimationController _animationController;
  late Animation<double> _opacityAnimation;
  late Animation<double> _translateAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _opacityAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
    _translateAnimation = Tween<double>(begin: 40, end: 0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggleMenu() {
    setState(() {
      _isExpanded = !_isExpanded;
      if (_isExpanded) {
        _animationController.forward();
      } else {
        _animationController.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.bottomRight,
      children: [
        // 展开的动作按钮
        if (_isExpanded)
          FadeTransition(
            opacity: _opacityAnimation,
            child: SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, 1),
                end: Offset.zero,
              ).animate(_animationController),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  _buildActionButton(Icons.camera, '拍照', () => print('拍照')),
                  const SizedBox(height: 16),
                  _buildActionButton(Icons.note_add, '新建笔记', () => print('新建笔记')),
                  const SizedBox(height: 16),
                ],
              ),
            ),
          ),
        // 主FAB
        FloatingActionButton(
          onPressed: _toggleMenu,
          child: Icon(_isExpanded ? Icons.close : Icons.add),
        ),
      ],
    );
  }

  Widget _buildActionButton(IconData icon, String label, VoidCallback onTap) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (_isExpanded)
          FadeTransition(
            opacity: _opacityAnimation,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              margin: const EdgeInsets.only(right: 16),
              decoration: BoxDecoration(
                color: Colors.grey.shade800,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                label,
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ),
        FloatingActionButton.small(
          onPressed: onTap,
          child: Icon(icon),
        ),
      ],
    );
  }
}

这个自定义实现里,我们用了FadeTransition和SlideTransition来实现动作按钮的淡入和滑入效果,完全符合Material Design的动效要求,你还可以根据自己的需求调整动画曲线、按钮样式、排列方式等。

两种方式各有优劣:第三方包省时间,自定义实现更灵活,你可以根据项目需求选择~

内容的提问来源于stack exchange,提问作者odonckers

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:22:55