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<DateTime> _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<TableRow> _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 <- 上月日期
4 5 6 7 8 9 10 <- 本月第一周
11 12 13 14 15 16 17 <- 本月第二周
18 19 20 21 22 23 24 <- 本月第三周
25 26 27 28 29 30 31 <- 本月第四周
1 2 3 4 5 6 7 <- 下月日期
不同的 CalendarFormat 只是改变了显示的行数:
- CalendarFormat.month: 显示整月(4-6行)
- CalendarFormat.twoWeeks: 显示2行
- CalendarFormat.week: 显示1行
每个日期单元格的具体样式由 dayBuilder 控制,可以通过 CalendarBuilders 和 CalendarStyle 来自定义外观。
这就是整个 TableCalendar 的界面渲染流程。它通过 PageView 实现翻页,通过 Table 实现网格布局,通过精确的日期计算确保日历的正确显示
其中函数_buildCalendarDays的解释如下:
让我们一步步解析:
- 函数声明:
- List 表示返回一个 TableRow 类型的列表
List.generate(count, (index) => value)
下划线 _ 开头表示这是一个私有方法
BuildContext context 是 Flutter 中用于构建 Widget 的上下文参数
- List 表示返回一个 TableRow 类型的列表
- 行数计算:
final rowAmount = visibleDays.length ~/ 7;
- ~/ 是整数除法运算符
比如如果 visibleDays.length 是 35,那么 rowAmount 就是 5
- List.generate:
List.generate(count, (index) => value)
这是 Dart 的列表生成方法
count 指定要生成多少个元素
(index) => value 是一个函数,用于生成每个元素 - TableRow:
- Flutter 中表格的一行
decoration 用于设置行的样式(比如背景色)
children 包含这一行的所有单元格
- Flutter 中表格的一行
- 嵌套的 List.generate:
- 生成每行的 7 个单元格
index * 7 + id 计算当前单元格对应的日期索引
dayBuilder 用于构建每个日期的显示内容
- 生成每行的 7 个单元格
举个例子,如果你要显示一个月的日历:
假设有 35 天要显示(5 周)
rowAmount 将是 5(35/7)
外层 List.generate 会生成 5 行
每行内部的 List.generate 会生成 7 个单元格
最终生成一个 5×7 的表格
这就像在创建一个 Excel 表格:
每个单元格的具体显示内容由 dayBuilder 决定,这就是为什么它是一个可自定义的函数。
生成视图的流程
- 初始化阶段:
// 在测试代码中初始化 TableCalendarBase
TableCalendarBase(
dayBuilder: (context, day, focusedDay) {
return Text(
'${day.day}',
key: dateToKey(day),
);
},
// ... 其他参数
)
- CalendarCore 中的包装:
// CalendarCore 给每个日期添加固定高度的容器
dayBuilder: (context, day) {
// ... 计算 baseDay ...
return SizedBox(
height: constrainedRowHeight ?? rowHeight,
child: dayBuilder(context, day, baseDay), // 调用原始的 dayBuilder
);
}
- CalendarPage 中的布局:
// CalendarPage 将日期排列成表格
List<TableRow> _buildCalendarDays(BuildContext context) {
return List.generate(
rowAmount, // 行数
(index) => TableRow(
children: List.generate(
7, // 每行7列
(id) => dayBuilder(context, visibleDays[index * 7 + id]), // 调用包装后的 dayBuilder
),
),
);
}
所以完整流程是:
用户提供基础的日期显示方式(Text组件)
CalendarCore 添加大小控制(SizedBox)
CalendarPage 将所有日期组织成表格形式(Table和TableRow)
最终形成一个完整的日历视图
这就像搭积木:
Text(显示日期)
→ SizedBox(控制大小)
→ TableRow(排成一行)
→ Table(组成表格)
→ 完整日历