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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:22:37