flutter: table calendar笔记

06-01 807阅读

pub dev:table_calendar 3.2.0

我来详细解释 TableCalendar 是如何根据不同的 CalendarFormat 来显示界面的。主要逻辑在 CalendarCore 中实现。

核心逻辑分为以下几个部分:

  1. 页面数量计算 - _getPageCount 方法根据不同格式计算总页数:
int _getPageCount(CalendarFormat format, DateTime first, DateTime last) {
  switch (format) {
    case CalendarFormat.month:
      return _getMonthCount(first, last) + 1;
    case CalendarFormat.twoWeeks:
      return _getTwoWeekCount(first, last) + 1;
    case CalendarFormat.week:
      return _getWeekCount(first, last) + 1;
  }
}
  1. 可见日期范围计算 - _getVisibleRange 方法决定每种格式显示多少天:
DateTimeRange _getVisibleRange(CalendarFormat format, DateTime focusedDay) {
  switch (format) {
    case CalendarFormat.month:
      return _daysInMonth(focusedDay);  // 显示整月
    case CalendarFormat.twoWeeks:
      return _daysInTwoWeeks(focusedDay);  // 显示两周
    case CalendarFormat.week:
      return _daysInWeek(focusedDay);  // 显示一周
  }
}
  1. 行数计算 - _getRowCount 方法决定显示多少行:
int _getRowCount(CalendarFormat format, DateTime focusedDay) {
  if (format == CalendarFormat.twoWeeks) {
    return 2;  // 两周显示2行
  } else if (format == CalendarFormat.week) {
    return 1;  // 一周显示1行
  } else if (sixWeekMonthsEnforced) {
    return 6;  // 月视图强制6行
  }
  // 月视图动态计算行数
  final first = _firstDayOfMonth(focusedDay);
  final daysBefore = _getDaysBefore(first);
  final firstToDisplay = first.subtract(Duration(days: daysBefore));
  final last = _lastDayOfMonth(focusedDay);
  final daysAfter = _getDaysAfter(last);
  final lastToDisplay = last.add(Duration(days: daysAfter));
  return (lastToDisplay.difference(firstToDisplay).inDays + 1) ~/ 7;
}
  1. 日期生成 - 根据不同格式生成要显示的日期:
// 生成一周的日期
DateTimeRange _daysInWeek(DateTime focusedDay) {
  final daysBefore = _getDaysBefore(focusedDay);
  final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore));
  final lastToDisplay = firstToDisplay.add(const Duration(days: 7));
  return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
}
// 生成两周的日期
DateTimeRange _daysInTwoWeeks(DateTime focusedDay) {
  final daysBefore = _getDaysBefore(focusedDay);
  final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore));
  final lastToDisplay = firstToDisplay.add(const Duration(days: 14));
  return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
}
// 生成一个月的日期
DateTimeRange _daysInMonth(DateTime focusedDay) {
  final first = _firstDayOfMonth(focusedDay);
  final daysBefore = _getDaysBefore(first);
  final firstToDisplay = first.subtract(Duration(days: daysBefore));
  if (sixWeekMonthsEnforced) {
    // 强制显示6周
    final end = firstToDisplay.add(const Duration(days: 42));
    return DateTimeRange(start: firstToDisplay, end: end);
  }
  // 根据实际天数显示
  final last = _lastDayOfMonth(focusedDay);
  final daysAfter = _getDaysAfter(last);
  final lastToDisplay = last.add(Duration(days: daysAfter));
  return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
}
  1. 页面切换逻辑 - _getFocusedDay 方法处理不同格式下的页面切换:
DateTime _getFocusedDay(
  CalendarFormat format,
  DateTime prevFocusedDay,
  int pageIndex,
) {
  if (pageIndex == previousIndex) {
    return prevFocusedDay;
  }
  final pageDif = pageIndex - previousIndex!;
  DateTime day;
  switch (format) {
    case CalendarFormat.month:
      // 月视图按月切换
      day = DateTime.utc(prevFocusedDay.year, prevFocusedDay.month + pageDif);
    case CalendarFormat.twoWeeks:
      // 两周视图每次移动14天
      day = DateTime.utc(
        prevFocusedDay.year,
        prevFocusedDay.month,
        prevFocusedDay.day + pageDif * 14,
      );
    case CalendarFormat.week:
      // 周视图每次移动7天
      day = DateTime.utc(
        prevFocusedDay.year,
        prevFocusedDay.month,
        prevFocusedDay.day + pageDif * 7,
      );
  }
  // 确保不超出范围
  if (day.isBefore(firstDay)) {
    day = firstDay;
  } else if (day.isAfter(lastDay)) {
    day = lastDay;
  }
  return day;
}

如果你想添加新的 CalendarFormat(比如 threeWeeks),需要:

  • 在 utils.dart 中的 CalendarFormat 枚举中添加新格式
  • 在上述所有 switch 语句中添加新的 case 处理
  • 实现对应的日期范围计算方法
  • 添加行数计算逻辑

    例如添加 threeWeeks 格式

    // 1. 在 CalendarFormat 中添加
    enum CalendarFormat { month, twoWeeks, week, threeWeeks }
    // 2. 在 switch 语句中添加处理
    case CalendarFormat.threeWeeks:
      return _daysInThreeWeeks(focusedDay);
    // 3. 实现日期范围计算
    DateTimeRange _daysInThreeWeeks(DateTime focusedDay) {
      final daysBefore = _getDaysBefore(focusedDay);
      final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore));
      final lastToDisplay = firstToDisplay.add(const Duration(days: 21));
      return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
    }
    // 4. 添加行数计算
    if (format == CalendarFormat.threeWeeks) {
      return 3;  // 三周显示3行
    }
    

    TableCalendar 的界面绘制流程

    让我详细解释 TableCalendar 的界面绘制流程。整个日历界面的绘制主要涉及以下组件的配合:

    TableCalendar (主组件) -> CalendarCore (核心布局) -> CalendarPage (单页渲染)

    让我们从最内层的 CalendarPage 开始解释:

    1. CalendarPage - 单个日历页面的渲染
    class CalendarPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            // 1. 可选的周数显示列
            if (weekNumberVisible) _buildWeekNumbers(context),
            
            // 2. 主日历表格
            Expanded(
              child: Table(
                border: tableBorder,
                children: [
                  // 2.1 星期头部行 (Mon, Tue, Wed...)
                  if (dowVisible) _buildDaysOfWeek(context),
                  // 2.2 日期行
                  ..._buildCalendarDays(context),
                ],
              ),
            ),
          ],
        );
      }
    }
    

    日历页面使用 Table widget 来布局,包含:

    可选的周数列

    星期头部行

    日期网格

    2. CalendarCore - 页面管理与切换

    class CalendarCore extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return PageView.builder(
          controller: pageController,
          itemCount: _getPageCount(calendarFormat, firstDay, lastDay),
          itemBuilder: (context, index) {
            // 1. 计算这一页要显示的基准日期
            final baseDay = _getBaseDay(calendarFormat, index);
            
            // 2. 计算这一页要显示的日期范围
            final visibleRange = _getVisibleRange(calendarFormat, baseDay);
            
            // 3. 生成要显示的所有日期
            final visibleDays = _daysInRange(visibleRange.start, visibleRange.end);
            // 4. 计算行高
            final actualDowHeight = dowVisible ? dowHeight! : 0.0;
            final constrainedRowHeight = constraints.hasBoundedHeight
                ? (constraints.maxHeight - actualDowHeight) / _getRowCount(calendarFormat, baseDay)
                : null;
            // 5. 渲染日历页面
            return CalendarPage(
              visibleDays: visibleDays,
              // ... 其他配置
            );
          },
        );
      }
    }
    
    1. 实际的渲染过程

      以月视图为例,完整的渲染流程:

    // 1. 确定要显示的日期范围
    DateTimeRange _daysInMonth(DateTime focusedDay) {
      // 获取月份第一天
      final first = _firstDayOfMonth(focusedDay);  // 例如:2024-03-01
      
      // 计算需要显示的上月剩余天数
      final daysBefore = _getDaysBefore(first);    // 如果3月1日是周五,需要显示前4天
      
      // 计算本月第一个显示的日期
      final firstToDisplay = first.subtract(Duration(days: daysBefore));  // 2024-02-26
      
      if (sixWeekMonthsEnforced) {
        // 强制显示6周
        final end = firstToDisplay.add(const Duration(days: 42));  // 显示42天
        return DateTimeRange(start: firstToDisplay, end: end);
      }
      // 获取月份最后一天
      final last = _lastDayOfMonth(focusedDay);    // 2024-03-31
      
      // 计算需要显示的下月天数
      final daysAfter = _getDaysAfter(last);       // 补充到完整的周
      
      // 计算最后一个显示的日期
      final lastToDisplay = last.add(Duration(days: daysAfter));  // 2024-04-06
      return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
    }
    // 2. 生成所有要显示的日期
    List _daysInRange(DateTime first, DateTime last) {
      final dayCount = last.difference(first).inDays + 1;
      return List.generate(
        dayCount,
        (index) => DateTime.utc(first.year, first.month, first.day + index),
      );
    }
    // 3. 渲染表格
    List _buildCalendarDays(BuildContext context) {
      final rowAmount = visibleDays.length ~/ 7;  // 计算行数
      return List.generate(
        rowAmount,
        (row) => TableRow(
          decoration: rowDecoration,
          children: List.generate(
            7,
            (column) => dayBuilder(context, visibleDays[row * 7 + column]),
          ),
        ),
      );
    }
    

    视觉效果示意:

         March 2024
    Mo Tu We Th Fr Sa Su
    26 27 28 29  1  2  3  
        return Text(
          '${day.day}',
          key: dateToKey(day),
        );
      },
      // ... 其他参数
    )
    
      // ... 计算 baseDay ...
      return SizedBox(
        height: constrainedRowHeight ?? rowHeight,
        child: dayBuilder(context, day, baseDay),  // 调用原始的 dayBuilder
      );
    }
    
      return List.generate(
        rowAmount,  // 行数
        (index) = TableRow(
          children: List.generate(
            7,  // 每行7列
            (id) = dayBuilder(context, visibleDays[index * 7 + id]),  // 调用包装后的 dayBuilder
          ),
        ),
      );
    }
    
      final DateTime focusedDay;
      final CalendarFormat calendarFormat;
      final DateTime firstDay;
      final DateTime lastDay;
      // ... 其他配置参数
    }
    
      late PageController _pageController;
      // 处理页面切换
      void _onPageChanged(int index) { ... }
      // 计算页面位置
      int _calculateFocusedPage() { ... }
    }
    
      // 计算可见日期范围
      DateTimeRange _getVisibleRange() { ... }
      // 构建页面
      Widget build(BuildContext context) {
        return PageView.builder(
          itemBuilder: (context, index) = CalendarPage(...)
        );
      }
    }
    
  • CalendarPage (StatelessWidget)

    • 负责单个日历页面的渲染
    • 构建表格布局
    • 渲染日期单元格
      class CalendarPage extends StatelessWidget {
        // 构建星期头部
        TableRow _buildDaysOfWeek() { ... }
        // 构建日期网格
        List _buildCalendarDays() { ... }
      }
      
    • 3. 数据流向

    用户交互
       ↓
    TableCalendarBase
       ↓
    _TableCalendarBaseState
       ↓
    CalendarCore
       ↓
    CalendarPage
    

    4. 具体工作流程

    1. 初始化:

      // TableCalendarBase 创建
      TableCalendarBase(
        focusedDay: DateTime.now(),
        calendarFormat: CalendarFormat.month,
        // ... 其他配置
      )
      
    2. 状态管理:

      // _TableCalendarBaseState 初始化
      void initState() {
        _pageController = PageController(
          initialPage: _calculateFocusedPage(...)
        );
      }
      
    3. 页面构建:

      // CalendarCore 构建页面
      Widget build(BuildContext context) {
        return PageView.builder(
          controller: _pageController,
          itemBuilder: (context, index) {
            // 计算日期范围
            final visibleRange = _getVisibleRange(...);
            // 构建页面
            return CalendarPage(
              visibleDays: _daysInRange(...),
              // ... 其他参数
            );
          }
        );
      }
      
    4. 渲染日历:

      // CalendarPage 渲染表格
      Widget build(BuildContext context) {
        return Table(
          children: [
            _buildDaysOfWeek(),  // 星期头部
            ..._buildCalendarDays(),  // 日期网格
          ],
        );
      }
      

    5. 关键交互流程

    1. 页面切换:

      用户滑动 → PageView 触发 → _TableCalendarBaseState 处理 → 更新 focusedDay
      
    2. 日期选择:

      用户点击日期 → CalendarPage 触发 → _TableCalendarBaseState 处理 → 更新状态
      
    3. 格式切换:

      用户切换格式 → TableCalendarBase 更新 → CalendarCore 重新计算 → 更新显示
      

    这种分层设计的好处是:

    1. 职责分离,每个类都有明确的职责
    2. 状态管理集中,便于维护
    3. 渲染逻辑分离,提高性能
    4. 便于扩展和自定义

    如果你想修改日历的某个特定部分,可以针对相应的类进行修改:

    • 修改整体行为 → TableCalendarBase
    • 修改状态管理 → _TableCalendarBaseState
    • 修改布局逻辑 → CalendarCore
    • 修改单元格样式 → CalendarPage
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码