深入源码:THCalendarDatePicker 日历网格布局算法是如何工作的?
深入源码:THCalendarDatePicker 日历网格布局算法是如何工作的?
【免费下载链接】THCalendarDatePickerA DatePicker based on a custom calendar view项目地址: https://gitcode.com/gh_mirrors/th/THCalendarDatePicker
THCalendarDatePicker 是一款基于自定义日历视图构建的 iOS 日期选择器(A DatePicker based on a custom calendar view)。很多开发者第一次打开它都会好奇:屏幕上那整齐的 7 列 × 6 行日历网格布局到底是怎么算出来的?每个月天数不同、一号落在星期几也不同,网格却永远不会乱。这篇文章将带你深入 THCalendarDatePicker 源码,逐行拆解日历网格布局算法的完整计算流程,无论你是新手还是想二次开发的工程师,都能快速看懂它的运作原理。
一、为什么日历网格布局是选择器的灵魂?
日历选择器的一切交互都建立在网格之上:日期被点击、被选中、被禁用、被标记圆点,全都发生在一个个等分的格子里。THCalendarDatePicker 选择了一个非常聪明的策略——网格永远固定为 7 列 × 6 行,一共 42 个格子,而不是像某些日历那样按月份实际周数动态伸缩。
固定网格带来两个好处:
- 布局稳定:无论 2 月只有 28 天还是 7 月有 31 天,控件的高度和宽度都不变,界面不会抖动;
- 渲染高效:每次翻页只需重绘固定数量的格子,配合滑动动画也不会出现明显的性能问题。
这套网格的核心逻辑全部集中在THCalendarDatePicker/THDatePickerViewController.m里,下面我们按“算参数 → 填格子 → 做对齐 → 管状态 → 加交互”五步来拆解。
二、网格布局三要素:7 列、6 行、起始偏移
打开redrawDays方法,你会发现网格的尺寸计算非常直接:
- 列宽:
cellWidth = areaWidth / 7,把容器宽度平均分成 7 份,对应一周 7 天; - 行高:
cellHeight = areaHeight / _weeksOnCalendar,而行数_weeksOnCalendar被硬编码为6; - 起始偏移:
_bufferDaysBeginning,记录本月 1 号之前需要预留的空格数量,这是整个布局算法里最关键的一个变量。
| 参数 | 取值 | 作用 |
|---|---|---|
| 列数 | 7 | 一周 7 天,cellWidth = areaWidth / 7 |
| 行数 | 6(固定) | _weeksOnCalendar = 6,共 42 格 |
| 起始偏移 | _bufferDaysBeginning | 本月 1 号前的空白格数量 |
三、起始空白格的计算方法:一号是星期几说了算
为什么有的月份 1 号在第一个格子,有的却要空出三四个格子?答案就在storeDateInformation方法里:
NSRange days = [c rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:self.firstOfCurrentMonth]; int bufferDaysBeginning = (int)([comps weekday]-[c firstWeekday]); if (bufferDaysBeginning < 0) bufferDaysBeginning += 7;它的逻辑是:取本月 1 号的weekday(周日为 1,周六为 7),减去日历的firstWeekday(默认也是周日 1),差值就是要预留的空白格数;如果结果是负数,就加上 7 修正——源码注释里还特意提到,% 7对负数取模在 Objective-C 中行为怪异,所以用了if判断来兜底。
举个例子:2015 年 7 月 1 日是周三(weekday = 4),那么bufferDaysBeginning = 4 - 1 = 3,于是 7 月前面会先画出 28、29、30 三个上个月的浅灰色日期,正好对应上图截图里第一行的效果。📐
四、42 个日期格子如何被逐一填充?
拿到偏移量之后,redrawDays就开始“铺格子”了,整体流程可以概括为:
- 找起点:从
firstOfCurrentMonth往回退_bufferDaysBeginning天,得到网格中第一个格子对应的日期; - 循环 42 次:每次创建一个
THDateDay视图(从THDateDay.xib加载),放进网格,并让它指向当前日期; - 前进与换行:每处理一格
curX += cellWidth;当索引i % 7 == 0时回到行首curX = origX,同时curY += cellHeight进入下一行; - 写日期数字:取出日期的
day分量,设置到按钮标题上。
核心循环长这样:
for(int i = 0; i < days; i++){ if(i && !(i%7)) { // 每 7 个换一行 curX = origX; curY += cellHeight; } THDateDay * day = [加载 THDateDay.xib]; day.frame = CGRectMake(curX, curY, cellWidth, cellHeight); day.date = date; // 设置标题、颜色、选中状态…… date = 加一天; // 日期向前推进 curX += cellWidth; }date = [_calendar dateByAddingComponents:offsetComponents toDate:date options:0]负责让日期每天加 1,网格填满 42 格时,正好覆盖“上月末尾 + 本月全部 + 下月开头”,这也是为什么网格里总会出现浅灰色的邻月日期——它们不是错误,而是为了让网格始终填满 6 行。🧩
五、居中与对齐:坐标计算的数学细节
细心的读者会发现,截图里网格四周是有留白的,这正是源码里两行“居中公式”的功劳:
int curY = (areaHeight - cellHeight*_weeksOnCalendar)/2; // 垂直居中 int origX = (areaWidth - cellWidth*7)/2; // 水平居中先用容器总尺寸减去网格总尺寸,再除以 2,得到的就是上下左右的边距。这样即使在不同屏幕宽度下,日历网格布局也能始终保持在容器正中央。
同样的偏移逻辑还被用在redrawWeekdays里:星期表头(Sun~Sat)用同一个dayWidth和相同的curX起点绘制,从而保证表头与下方每一列日期严格对齐。看这张横屏截图,网格和星期行的对齐关系一目了然:
六、单元格如何表达每一天的状态?
网格只是骨架,真正让日历“活”起来的是THDateDay这个单元格视图(源码位于THCalendarDatePicker/THDateDay.m)。它封装了每一天的多种视觉状态:
- 选中态:
setSelected:把背景染成selectedBackgroundColor,文字变白; - 邻月淡化:
setLightText:让非本月日期显示为浅灰色,区分主次; - 今天高亮:
isToday比对年月日,用currentDateColor给今天的数字上色; - 禁用态:
setEnabled:配合dateInFutureAndShouldBeDisabled:限制历史/未来日期可选; - 事件标记:
indicateDayHasItems:控制日期下方的小圆点,可用于提示“这一天有行程/数据”。
如果开启了rounded,addMaskToBounds:还会用圆形遮罩把格子裁成圆角,观感更柔和。🎨
七、网格之外的交互:点击、滑动与翻页动画
网格铺好后,交互层的设计同样值得学习:
- 点击选日:
THDateDay通过代理把点击事件上抛给dateDayTapped:,控制器再按单选、多选、区间三种模式(THDatePickerSelectionType)分别处理,选完还会回调给上层业务代码; - 滑动翻页:在
calendarDaysView上注册了四个方向的滑动手势——左右滑切换年份、上下滑切换月份(关闭年份切换时左右滑变月份); - 翻页动画:
slideTransitionViewInDirection:会创建一个新网格视图,让新旧两个视图同时反向滑动、交叉淡入淡出,整个过渡只有约 0.5 秒; - 智能跳月:点击邻月的浅灰色日期时,日历会自动滑动到对应月份,并保持选中该日期,体验非常顺滑。
想快速体验这些效果,克隆仓库后打开Examples/THCalendarDatePickerExample下的示例工程即可,TestViewController.m里演示了完整的配置流程,包括设置日期范围、禁用历史选择、自定义选中色与圆点回调等。
小结
回顾整个 THCalendarDatePicker 日历网格布局算法,核心其实只有四步:用 7 列固定 6 行撑起 42 格画布 → 根据一号星期几算出起始偏移 → 从“本月 1 号减偏移”的日期开始逐格填充 → 用居中公式保证整体对齐。理解了这套思路,你不仅能看懂这个项目的源码,以后再见到任何自定义日历控件,都能快速抓住它的布局命脉。希望这篇源码解析能帮你彻底吃透日历网格布局算法!🚀
【免费下载链接】THCalendarDatePickerA DatePicker based on a custom calendar view项目地址: https://gitcode.com/gh_mirrors/th/THCalendarDatePicker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
