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

如何使用CustomScrollView结合Slivers实现带月份联动锁定的水平日期滚动?

如何使用CustomScrollView结合Slivers实现带月份联动锁定的水平日期滚动?

嘿,我完全懂你要的这种效果——就是那种滑日期的时候,月份标题会跟着同步切换,到月与月的边界还能稳稳锁定住的日历滚动对吧?既然你明确要求必须用Slivers,那咱们就基于CustomScrollView来一步步搭,逻辑其实没那么复杂,拆解成几个核心步骤就行:

一、先把数据层理清楚

首先得把日期和对应的月份绑定好,不然滚动的时候没法快速判断当前属于哪个月。我一般会定义个轻量的模型类来关联:

// 绑定日期与所属月份的模型
class DateWithMonth {
  final DateTime date;
  final String monthName; // 比如"2024年6月"
  DateWithMonth(this.date, this.monthName);
}

提前把全年的日期按月份分组初始化好,比如用循环生成1-12月的所有日期,给每个日期打上对应月份的标签,这样后面滚动时能直接取到月份信息。

二、搭好CustomScrollView的Sliver基础结构

因为要做水平滚动的日期,所以把CustomScrollView的scrollDirection设为Axis.horizontal,然后塞两个核心Sliver:

  • 一个固定的月份标题SliverPersistentHeader:开启pinned: true让它始终停在垂直方向的顶部,不会跟着水平滚动跑。
  • 一个水平排列的日期列表SliverList:用来渲染所有日期项,宽度要和后面滚动计算的数值对应上。

基础结构代码大概是这样:

final ScrollController _scrollController = ScrollController();
String _currentMonth = "2024年6月";
List<DateWithMonth> _allDates = []; // 提前初始化好所有日期数据

@override
Widget build(BuildContext context) {
  return CustomScrollView(
    controller: _scrollController,
    scrollDirection: Axis.horizontal,
    slivers: [
      // 固定在顶部的月份标题头
      SliverPersistentHeader(
        pinned: true,
        delegate: MonthHeaderDelegate(
          monthName: _currentMonth,
          height: 55,
        ),
      ),
      // 水平滚动的日期列表Sliver
      SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) {
            final dateItem = _allDates[index];
            return Container(
              width: 65, // 每个日期项的固定宽度
              alignment: Alignment.center,
              padding: const EdgeInsets.symmetric(vertical: 12),
              child: Column(
                children: [
                  Text("${dateItem.date.day}", style: const TextStyle(fontSize: 16)),
                  Text(dateItem.date.weekday == 7 ? "日" : "${dateItem.date.weekday}", style: const TextStyle(fontSize: 12, color: Colors.grey)),
                ],
              ),
            );
          },
          childCount: _allDates.length,
        ),
      ),
    ],
  );
}

// 自定义月份Header的Delegate,控制SliverPersistentHeader的样式
class MonthHeaderDelegate extends SliverPersistentHeaderDelegate {
  final String monthName;
  final double height;

  MonthHeaderDelegate({required this.monthName, required this.height});

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      height: height,
      alignment: Alignment.center,
      color: Colors.white,
      child: Text(monthName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
    );
  }

  @override
  double get maxExtent => height;

  @override
  double get minExtent => height;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return (oldDelegate as MonthHeaderDelegate).monthName != monthName;
  }
}

三、实现月份与日期的联动锁定(核心逻辑)

这步是关键——我们要监听滚动位置,判断当前显示的核心区域属于哪个月,并且在月与月的交界处实现“锁定”效果:

  1. 先在initState里给滚动控制器加监听,记得 dispose 时销毁:
@override
void initState() {
  super.initState();
  _scrollController.addListener(_onScroll);
  // 这里可以加初始化_allDates的逻辑,比如生成2024年的所有日期
}

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}
  1. 实现_onScroll方法,计算当前可视区域核心位置对应的月份,实现联动与锁定:
void _onScroll() {
  final offset = _scrollController.offset;
  final dateItemWidth = 65.0; // 和SliverList里的日期项宽度完全一致
  final viewportWidth = MediaQuery.of(context).size.width;
  
  // 取可视区域的中间位置作为判断基准——这就是实现"锁定"的关键
  final middleOffset = offset + viewportWidth / 2;
  final currentIndex = (middleOffset / dateItemWidth).floor();

  // 边界判断,避免索引越界
  if (currentIndex >= 0 && currentIndex < _allDates.length) {
    final targetMonth = _allDates[currentIndex].monthName;
    // 只有当月份真的变化时才更新,避免频繁刷新UI
    if (targetMonth != _currentMonth) {
      setState(() {
        _currentMonth = targetMonth;
      });
    }
  }
}

这个逻辑的妙处在于:只有当可视区域的中间点进入下一个月的日期时,月份才会切换。滚动到月中时,中间点始终在当前月内,月份就会保持锁定,完全符合你要的效果!

四、优化细节让体验更丝滑

  • 给月份标题加个过渡动画:把MonthHeaderDelegate里的Text用AnimatedSwitcher包裹,切换月份时加个淡入淡出效果,视觉上更流畅。
  • 高亮当前日期:在SliverList的日期项里,判断如果是今天的日期,就给个不同的样式(比如红色文字+背景圆点)。
  • 处理边缘空白:在日期列表的首尾加几个空白的占位Sliver,避免第一个月的日期贴死左边界,最后一个月的日期贴死右边界。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 08:09:33