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

Flutter自定义横向日历:滚动切换月份时动态更新年月标题

实现横向日历滚动时同步更新年月标题

核心思路

通过ScrollController监听滚动事件,计算当前可视区域内的日期对应的年月,实时更新顶部标题;也可以用NotificationListener替代ScrollController监听滚动,逻辑一致。


1. 基础实现步骤

初始化ScrollController与数据

先创建滚动控制器、日期列表和用于展示的年月变量:

late ScrollController _scrollController;
final List<DateTime> _dateList = [];
String _currentYearMonth = '';

@override
void initState() {
  super.initState();
  _scrollController = ScrollController();
  // 生成全年365天的日期列表(按需调整闰年/日期范围)
  final startDate = DateTime(DateTime.now().year, 1, 1);
  for (int i = 0; i < 365; i++) {
    _dateList.add(startDate.add(Duration(days: i)));
  }
  // 初始化默认年月
  _currentYearMonth = _formatYearMonth(_dateList[0]);
  // 添加滚动监听
  _scrollController.addListener(_onScroll);
}

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

滚动监听与年月更新逻辑

在滚动回调里,获取可视区域的首个item对应的日期,判断是否切换了月份,更新标题:

void _onScroll() {
  final firstVisibleIndex = _scrollController.position.firstVisibleItemIndex;
  if (firstVisibleIndex == null || firstVisibleIndex >= _dateList.length) return;

  final currentDate = _dateList[firstVisibleIndex];
  final newYearMonth = _formatYearMonth(currentDate);
  // 仅当年月真正变化时更新,避免频繁刷新
  if (newYearMonth != _currentYearMonth) {
    setState(() {
      _currentYearMonth = newYearMonth;
    });
  }
}

// 格式化日期为「YYYY年MM月」格式
String _formatYearMonth(DateTime date) {
  return '${date.year}年${date.month.toString().padLeft(2, '0')}月';
}

页面布局

把年月标题和横向ListView组合起来:

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      // 顶部年月标题
      Padding(
        padding: const EdgeInsets.symmetric(vertical: 16),
        child: Text(
          _currentYearMonth,
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
      ),
      Expanded(
        child: ListView.builder(
          controller: _scrollController,
          scrollDirection: Axis.horizontal,
          itemCount: _dateList.length,
          itemBuilder: (context, index) {
            // 自定义日历item布局
            final date = _dateList[index];
            return Container(
              width: 60,
              alignment: Alignment.center,
              child: Column(
                children: [
                  Text(date.day.toString()),
                  Text('周${_getWeekDay(date)}'),
                ],
              ),
            );
          },
        ),
      ),
    ],
  );
}

// 辅助方法:获取星期简写
String _getWeekDay(DateTime date) {
  const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
  return weekdays[date.weekday % 7];
}

2. 优化:避免标题频繁跳动

如果滚动时每个item都触发更新,可能导致标题频繁切换。可以增加偏移判断,当item滚动过半且下一个item属于新月份时再更新:

void _onScroll() {
  final position = _scrollController.position;
  final firstVisibleIndex = position.firstVisibleItemIndex;
  if (firstVisibleIndex == null || firstVisibleIndex >= _dateList.length) return;

  final itemSize = 60; // 对应日历item的宽度
  final offset = position.pixels - firstVisibleIndex * itemSize;
  DateTime targetDate = _dateList[firstVisibleIndex];
  
  // 当item滚动超过一半,且下一个item存在时,用下一个item的日期判断
  if (offset > itemSize / 2) {
    final nextIndex = firstVisibleIndex + 1;
    if (nextIndex < _dateList.length) {
      targetDate = _dateList[nextIndex];
    }
  }

  final newYearMonth = _formatYearMonth(targetDate);
  if (newYearMonth != _currentYearMonth) {
    setState(() {
      _currentYearMonth = newYearMonth;
    });
  }
}

3. 性能优化:用ValueNotifier减少重建

频繁调用setState会触发整个页面重建,改用ValueNotifier仅更新标题部分:

final ValueNotifier<String> _yearMonthNotifier = ValueNotifier('');

// 初始化时替换为:
_yearMonthNotifier.value = _formatYearMonth(_dateList[0]);

// 更新时替换为:
_yearMonthNotifier.value = newYearMonth;

// 标题布局替换为:
ValueListenableBuilder<String>(
  valueListenable: _yearMonthNotifier,
  builder: (context, value, child) {
    return Text(
      value,
      style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
    );
  },
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 06:01:45