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
相关产品推荐
相关产品推荐

