Flutter实现类Gmail撰写按钮的FAB动画及文本平滑淡入效果
问题根源
你当前的实现没生效主要有3个核心原因:
- 当
flag为false(收缩状态)时,label属性直接返回了Icon组件,AnimatedOpacity组件被完全销毁,没有实例承载动画过渡 - 透明度切换和FAB容器的展开动画完全同步,没有等FAB 300ms的展开动画执行完成再触发文本淡入
- 你给
AnimatedOpacity设置的duration是9000毫秒(也就是9秒),时长太长视觉上几乎看不到动效,属于笔误问题
修复步骤
- 新增2个状态变量:一个控制文本透明度的
_textOpacity,一个用来缓存定时器避免重复触发 - 调整滚动监听逻辑:
- 切换为展开状态时,先执行FAB容器展开动画,等待300ms动画结束后再修改文本透明度为1,触发淡入
- 切换为收缩状态时,先修改文本透明度为0触发淡出,等待淡入动画结束后再修改flag更新FAB宽度
- 去掉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
相关产品推荐
相关产品推荐

