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 {@overrideWidget 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 {@overrideWidget 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-26if (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-06return 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(組成表格)
→ 完整日歷