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

Flutter如何无第三方库实现带动画的可展开悬浮操作按钮

Flutter 带动画效果的可展开Floating Action Button实现方案

以下方案无需依赖第三方插件,通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 06:36:27