Flutter实现Google Calendar左侧日期固定滑动动画方案咨询
实现Google Calendar左侧固定日期的滑动切换动画效果
嘿,刚好我之前研究过Google Calendar这个左侧日期的动画效果,给你分享下具体的实现思路和代码示例吧!
核心思路
这个效果的关键在于监听页面滚动偏移,根据滚动进度触发日期的过渡动画:
- 用
PageView管理不同日期的内容页面,支持左右滑动切换 - 左侧固定区域放置日期组件,通过
PageController监听滚动偏移量 - 根据滚动进度(当前页面到下一页的比例),对当前日期和下一个日期做透明度、位置偏移的动画,当滚动超过50%时完成日期切换
具体实现步骤
1. 准备日期数据
先生成一组连续的日期(比如最近7天),作为PageView的数据源:
final today = DateTime.now(); _dates = List.generate(7, (index) => today.add(Duration(days: index)));
2. 构建页面结构
页面分为左右两部分:左侧固定日期区域,右侧可滑动的内容区域:
- 左侧用
SizedBox固定宽度,内部通过Stack叠加当前日期和下一个日期,实现过渡动画 - 右侧用
Expanded包裹PageView.builder,加载对应日期的内容
3. 监听滚动状态
通过PageController的addListener获取实时滚动偏移,计算当前页面和滚动进度:
_pageController = PageController() ..addListener(() { setState(() { _scrollOffset = _pageController.page!; _currentPage = _scrollOffset.floor(); }); });
4. 实现日期过渡动画
利用Opacity和Transform.translate组件,根据滚动进度控制两个日期的显示状态:
- 当前日期:随着滚动进度增加,透明度降低,向上偏移
- 下一个日期:随着滚动进度增加,透明度升高,向下偏移(从下方滑入)
完整代码示例
import 'package:flutter/material.dart'; class CalendarDateAnimation extends StatefulWidget { const CalendarDateAnimation({super.key}); @override State<CalendarDateAnimation> createState() => _CalendarDateAnimationState(); } class _CalendarDateAnimationState extends State<CalendarDateAnimation> { late PageController _pageController; List<DateTime> _dates = []; int _currentPage = 0; double _scrollOffset = 0.0; @override void initState() { super.initState(); // 生成最近7天的日期数据 final today = DateTime.now(); _dates = List.generate(7, (index) => today.add(Duration(days: index))); // 初始化PageController并监听滚动 _pageController = PageController() ..addListener(() { setState(() { _scrollOffset = _pageController.page!; _currentPage = _scrollOffset.floor(); }); }); } @override void dispose() { _pageController.dispose(); super.dispose(); } // 格式化日期为"周一\n10/16"的样式 String _formatDate(DateTime date) { final weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; final weekDay = weekDays[date.weekday % 7]; final month = date.month.toString().padLeft(2, '0'); final day = date.day.toString().padLeft(2, '0'); return '$weekDay\n$month/$day'; } @override Widget build(BuildContext context) { return Scaffold( body: Row( children: [ // 左侧固定日期区域 SizedBox( width: 80, child: Center( child: Stack( alignment: Alignment.center, children: [ // 当前显示的日期 Opacity( opacity: 1 - (_scrollOffset - _currentPage).clamp(0.0, 1.0), child: Transform.translate( offset: Offset(0, -20 * (_scrollOffset - _currentPage)), child: Text( _formatDate(_dates[_currentPage]), textAlign: TextAlign.center, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), ), // 下一个要切换的日期(滚动时显示) if (_currentPage < _dates.length - 1) Opacity( opacity: (_scrollOffset - _currentPage).clamp(0.0, 1.0), child: Transform.translate( offset: Offset(0, 20 * (1 - (_scrollOffset - _currentPage))), child: Text( _formatDate(_dates[_currentPage + 1]), textAlign: TextAlign.center, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), ), ], ), ), ), // 右侧可滑动的日程内容区域 Expanded( child: PageView.builder( controller: _pageController, itemCount: _dates.length, itemBuilder: (context, index) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 8, offset: Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _formatDate(_dates[index]), style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.blueGrey, ), ), const SizedBox(height: 30), // 示例日程内容,可替换为真实数据 const Text( '今日日程:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 12), _buildScheduleItem('10:00', '团队周会'), _buildScheduleItem('14:30', '项目需求评审'), _buildScheduleItem('16:00', '客户沟通'), ], ), ); }, ), ), ], ), ); } // 构建单个日程项 Widget _buildScheduleItem(String time, String content) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( time, style: const TextStyle(color: Colors.grey, fontSize: 14), ), const SizedBox(width: 16), Expanded( child: Text( content, style: const TextStyle(fontSize: 15), ), ), ], ), ); } }
优化与自定义建议
- 动画参数调整:可以修改
Transform.translate的偏移量(比如把20改成15),或者调整透明度的变化曲线,让动画更贴近Google Calendar的原生效果 - 边界处理:代码里已经判断了
_currentPage < _dates.length - 1,避免滚动到最后一页时显示不存在的日期;同理也可以处理第一页的反向滚动 - 样式自定义:可以给选中的日期添加高亮样式(比如不同的颜色、背景框),或者调整字体大小、间距
- 性能优化:如果日期数量较多,建议给PageView的子页面加上
AutomaticKeepAliveClientMixin,避免页面切换时频繁重建
你可以直接把这段代码放到你的Flutter项目里运行,就能看到和Google Calendar类似的左侧日期滑动切换动画啦!
内容的提问来源于stack exchange,提问作者samuelchan727
相关产品推荐
相关产品推荐

