Flutter页面滚动交互效果实现求助(参考MyFitnessPal)
Flutter 实现类似 MyFitnessPal 首页的滚动交互效果
核心实现思路
- 通过监听滚动偏移量,动态控制页面中各组件的位置、缩放状态,实现联动动画
- 底部固定footer直接借助
Scaffold的bottomNavigationBar实现,无需额外处理滚动逻辑 - 采用
NotificationListener捕获滚动事件,配合Transform组件完成组件的位移、缩放动画
具体实现步骤
- 滚动监听:用
NotificationListener<ScrollNotification>包裹滚动容器,实时获取滚动距离 - 固定Footer:在
Scaffold中配置bottomNavigationBar,确保底部导航栏不随页面滚动 - 黄色半圆动画:根据滚动距离,逐步缩小半圆尺寸并向左上角偏移,直至完全移出视野
- 标题滑出:当滚动超过设定阈值,让标题和描述向上偏移,最终消失在屏幕外
- 按钮与头像布局调整:滚动到特定位置后,将“Go Premium”按钮向左上移动,与头像并排显示,同时展示下方所有卡片
关键代码示例
class MyFitnessPalHome extends StatefulWidget { const MyFitnessPalHome({super.key}); @override State<MyFitnessPalHome> createState() => _MyFitnessPalHomeState(); } class _MyFitnessPalHomeState extends State<MyFitnessPalHome> { double _scrollOffset = 0.0; // 自定义动画阈值,可根据设计需求调整 final double _titleHideThreshold = 200.0; final double _buttonMoveThreshold = 150.0; final double _circleDisappearThreshold = 100.0; @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener<ScrollNotification>( onNotification: (notification) { setState(() { _scrollOffset = notification.metrics.pixels; }); return true; }, child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Stack( children: [ // 左上角黄色半圆背景 Transform.translate( offset: Offset( -_scrollOffset * 0.8, -_scrollOffset * 0.8, ), child: Transform.scale( scale: 1 - (_scrollOffset / _circleDisappearThreshold).clamp(0, 1), child: Container( width: MediaQuery.of(context).size.width * 0.6, height: MediaQuery.of(context).size.width * 0.6, decoration: const BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.only( bottomRight: Radius.circular(300), ), ), ), ), ), // 标题与描述区域 Transform.translate( offset: Offset(0, -_scrollOffset.clamp(0, _titleHideThreshold)), child: Padding( padding: const EdgeInsets.only(top: 120, left: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text( "JustLift", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text("Track your workouts and reach goals"), ], ), ), ), // 头像与Premium按钮区域 Positioned( top: 60, right: 20, child: Row( children: [ // 滚动到阈值后显示并移动按钮 if (_scrollOffset > _buttonMoveThreshold) Transform.translate( offset: Offset( -(_scrollOffset - _buttonMoveThreshold) * 0.6, -(_scrollOffset - _buttonMoveThreshold) * 0.3, ), child: ElevatedButton( onPressed: () {}, child: const Text("Go Premium"), ), ), const SizedBox(width: 10), const CircleAvatar( radius: 25, // 替换为实际头像资源 backgroundImage: AssetImage("assets/avatar.png"), ), ], ), ), // 下方卡片列表 Padding( padding: EdgeInsets.only( top: 280 - _scrollOffset.clamp(0, 100), left: 20, right: 20, bottom: 80, // 给底部footer留空间 ), child: Column( children: [ Card(child: ListTile(title: const Text("Workout Plan"), subtitle: const Text("Today's routine"))), Card(child: ListTile(title: const Text("Nutrition"), subtitle: const Text("Calories tracked"))), Card(child: ListTile(title: const Text("Progress"), subtitle: const Text("Weekly stats"))), // 可添加更多卡片 ], ), ), ], ), ), ], ), ), // 固定底部Footer bottomNavigationBar: Container( height: 60, color: Colors.white, child: const Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Icon(Icons.home), Icon(Icons.bar_chart), Icon(Icons.person), ], ), ), ); } }
优化提示
- 可以调整动画系数(比如
_scrollOffset * 0.8中的0.8),让动画速率更贴合设计预期 - 若需要更复杂的动画逻辑,可结合
AnimationController和ScrollController实现精细控制 - 用
clamp(0, maxValue)限制偏移量范围,避免组件出现异常位移
内容的提问来源于stack exchange,提问作者Promaster
相关产品推荐
相关产品推荐

