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

Flutter实现类Gmail撰写按钮的FAB动画及文本平滑淡入效果

问题根源

你当前的实现没生效主要有3个核心原因:

  • 当flag为false(收缩状态)时,label属性直接返回了Icon组件,AnimatedOpacity组件被完全销毁,没有实例承载动画过渡
  • 透明度切换和FAB容器的展开动画完全同步,没有等FAB 300ms的展开动画执行完成再触发文本淡入
  • 你给AnimatedOpacity设置的duration是9000毫秒(也就是9秒),时长太长视觉上几乎看不到动效,属于笔误问题
修复步骤
  1. 新增2个状态变量:一个控制文本透明度的_textOpacity,一个用来缓存定时器避免重复触发
  2. 调整滚动监听逻辑:
    • 切换为展开状态时,先执行FAB容器展开动画,等待300ms动画结束后再修改文本透明度为1,触发淡入
    • 切换为收缩状态时,先修改文本透明度为0触发淡出,等待淡入动画结束后再修改flag更新FAB宽度
  3. 去掉label的条件分支切换组件的逻辑,统一保留Icon和Text组件,仅通过透明度控制文本显隐,保证动画组件一直挂载
完整修改后代码
import 'dart:async';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool isLoaded = false;
  bool upDirection = true, flag = true;
  final ScrollController _scrollController = ScrollController();
  double _textOpacity = 1.0;
  Timer? _fabTimer;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      final newUpDirection = _scrollController.position.userScrollDirection == ScrollDirection.forward;
      if (newUpDirection == flag) return;
      // 清除之前的定时器避免重复触发
      _fabTimer?.cancel();
      if (newUpDirection) {
        // 向下滚动:先展开FAB
        setState(() {
          flag = newUpDirection;
        });
        // 等待FAB展开完成后淡入文本
        _fabTimer = Timer(const Duration(milliseconds: 300), () {
          setState(() {
            _textOpacity = 1.0;
          });
        });
      } else {
        // 向上滚动:先淡出文本
        setState(() {
          _textOpacity = 0.0;
        });
        // 等待文本淡出完成后收缩FAB
        _fabTimer = Timer(const Duration(milliseconds: 300), () {
          setState(() {
            flag = newUpDirection;
          });
        });
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _fabTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 测试用的长列表,替换成你自己的页面内容
          ListView.builder(
            controller: _scrollController,
            itemCount: 100,
            itemBuilder: (context, index) => ListTile(title: Text("测试内容 $index")),
          ),
          Positioned(
            bottom: MediaQuery.of(context).size.height * 0.1,
            right: 20.0,
            child: AnimatedContainer(
              width: flag ? 170 : 56,
              height: 56,
              duration: const Duration(milliseconds: 300),
              child: FloatingActionButton.extended(
                backgroundColor: Colors.red, // 可替换为你自己的AppColors.customFabRed
                heroTag: null,
                onPressed: () {},
                icon: const Icon(Icons.call, color: Colors.white),
                label: AnimatedOpacity(
                  opacity: _textOpacity,
                  duration: const Duration(milliseconds: 300),
                  child: const Text(
                    'Call a doctor',
                    textAlign: TextAlign.start,
                    style: TextStyle(
                      fontSize: 13.5,
                      height: 1.5,
                      fontWeight: FontWeight.w400,
                      fontFamily: 'Euclid',
                      color: Colors.white,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 01:54:03