flutter: table calendar笔记
pub dev:table_calendar 3.2.0
我来详细解释 TableCalendar 是如何根据不同的 CalendarFormat 来显示界面的。主要逻辑在 CalendarCore 中实现。
核心逻辑分为以下几个部分:
- 页面数量计算 - _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; } }
- 可见日期范围计算 - _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); // 显示一周 } }
- 行数计算 - _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; }
- 日期生成 - 根据不同格式生成要显示的日期:
// 生成一周的日期 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); }
- 页面切换逻辑 - _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 开始解释:
- 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. 确定要显示的日期范围 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. 具体工作流程
-
初始化:
// TableCalendarBase 创建 TableCalendarBase( focusedDay: DateTime.now(), calendarFormat: CalendarFormat.month, // ... 其他配置 )
-
状态管理:
// _TableCalendarBaseState 初始化 void initState() { _pageController = PageController( initialPage: _calculateFocusedPage(...) ); }
-
页面构建:
// CalendarCore 构建页面 Widget build(BuildContext context) { return PageView.builder( controller: _pageController, itemBuilder: (context, index) { // 计算日期范围 final visibleRange = _getVisibleRange(...); // 构建页面 return CalendarPage( visibleDays: _daysInRange(...), // ... 其他参数 ); } ); }
-
渲染日历:
// CalendarPage 渲染表格 Widget build(BuildContext context) { return Table( children: [ _buildDaysOfWeek(), // 星期头部 ..._buildCalendarDays(), // 日期网格 ], ); }
5. 关键交互流程
-
页面切换:
用户滑动 → PageView 触发 → _TableCalendarBaseState 处理 → 更新 focusedDay
-
日期选择:
用户点击日期 → CalendarPage 触发 → _TableCalendarBaseState 处理 → 更新状态
-
格式切换:
用户切换格式 → TableCalendarBase 更新 → CalendarCore 重新计算 → 更新显示
这种分层设计的好处是:
- 职责分离,每个类都有明确的职责
- 状态管理集中,便于维护
- 渲染逻辑分离,提高性能
- 便于扩展和自定义
如果你想修改日历的某个特定部分,可以针对相应的类进行修改:
- 修改整体行为 → TableCalendarBase
- 修改状态管理 → _TableCalendarBaseState
- 修改布局逻辑 → CalendarCore
- 修改单元格样式 → CalendarPage
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。