当前位置: 首页 > news >正文

深入源码: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就开始“铺格子”了,整体流程可以概括为:

  1. 找起点:从firstOfCurrentMonth往回退_bufferDaysBeginning天,得到网格中第一个格子对应的日期;
  2. 循环 42 次:每次创建一个THDateDay视图(从THDateDay.xib加载),放进网格,并让它指向当前日期;
  3. 前进与换行:每处理一格curX += cellWidth;当索引i % 7 == 0时回到行首curX = origX,同时curY += cellHeight进入下一行;
  4. 写日期数字:取出日期的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:控制日期下方的小圆点,可用于提示“这一天有行程/数据”。

如果开启了roundedaddMaskToBounds:还会用圆形遮罩把格子裁成圆角,观感更柔和。🎨

七、网格之外的交互:点击、滑动与翻页动画

网格铺好后,交互层的设计同样值得学习:

  • 点击选日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),仅供参考

http://www.cnnetsun.cn/news/4123373.html

相关文章:

  • 老款Mac焕新三步走:用OpenCore Legacy Patcher免费升级最新macOS完整指南
  • DesktopAssistant打包发布实战:生成Windows安装程序与deb包的完整步骤
  • ZCode与DeepSeek集成指南:打造本地AI编程助手
  • Dtims CPB4612-LF 控制板
  • 对比评测:GifHub vs 其他 GitHub GIF 插件,谁才是评论区动图之王?
  • TypeScript-New-Handbook 新手教程:10 分钟理解静态类型检查的核心理念
  • TypeScript-New-Handbook 完全指南:微软官方 TypeScript 新手册,一篇读懂全貌
  • 磁盘清理工具免费推荐:开源 Czkawka,把重复文件与相似图片一次清干净
  • THCalendarDatePicker 手势交互揭秘:滑动切换月份与年份的 5 个实现细节
  • 基于SpringBoot+Vue的个性化音乐平台网站(源码+讲解视频+LW)
  • obs-shaderfilter扩展像素功能解析:轻松实现投影与边框的渲染技巧
  • 【软考信息安全】密码学考点“涮“明白!5张美食思维导图带你一锅端
  • 给 Copilot for Xcode 开发自定义工具:3 个实战案例,把 AI 助手调教成真正会干活的搭档
  • ECC 智能体钩子系统架构解析:如何为 AI 编码 Agent 装上“会记忆、能拦截、可审计“的行为护栏
  • 洛雪音乐音源怎么装最省心?免费无损音源合集实测导入指南
  • 免费通用设计字体 Morisawa BIZ UDGothic 上手教程:三步搞定下载、安装与排版
  • FIFA 23 修改器完整免费指南:从首条脚本到高阶模组,快速掌控生涯模式全细节
  • 微信聊天记录终极备份指南:用WeChatMsg免费导出HTML、Word、CSV与年度报告
  • ZoneMTA 智能退信处理:如何识别灰名单、拒收与 IP 拉黑
  • 小芯片运行模型时的资源边界
  • vue-star-rating 无障碍实践:用 screen-reader 插槽提升评分的可访问性
  • gruf 性能调优指南:线程池与服务器参数最佳实践
  • 基于微信小程序的剧本杀预约系统(源码+lw+部署文档+讲解等)
  • Dolphin 管理员模式安全指南:如何以 root 权限管理系统文件而不翻车
  • Mac视频预览终极方案:QLVideo让Finder缩略图和空格预览彻底告别格式限制
  • ReGreet配置完全指南:背景、时钟、主题与字体终极自定义清单
  • 小白也能学会!新能源汽车充电小程序支付漏洞挖掘与安全学习(收藏版)
  • 171、Zephyr RTOS调试与测试基础:性能分析工具
  • TabSTAR数据处理全流程揭秘:从原始表格到模型输入的5个关键步骤
  • Awesome-Mixture-of-Experts-Papers研究前沿:MoE未来趋势与5大值得关注的开放问题