CalendarView:纯 Canvas 绘制的高性能可定制日历库
日历是很多 App 的刚需——签到、预约、酒店预订、账单、打卡。但 Android 上做日历有两个老大难:性能差(传统基于 RecyclerView/GridView 的日历,一个月视图就要几百个 View,滑动卡顿)和难定制(UI 千篇一律,改源码成本高)。huanghaibin-dev 的 CalendarView 用一个反直觉的思路同时解决这两个问题——纯 Canvas 绘制 + 插拔式 UI:框架只管逻辑不画 UI,UI 全靠你继承 MonthView 用 Canvas 自定义。一个月视图的 View 数从 382 降到个位数,性能与定制自由度同时拉满。参考 GitHub 仓库。
1、概述:CalendarView 是什么
官方一句口号:"RecyclerView? ListView or ViewGroup? No, it's Canvas!"(不是 RecyclerView、不是 ListView、不是 ViewGroup,是 Canvas!)。它的核心是用纯 Canvas 绘制换取极致性能,再用插拔式设计让 UI 完全交给客户端自定义。
定位 纯 Canvas 绘制的 Android 日历库
核心机制 Canvas 直绘 + 插拔式 UI(框架不画 UI)
性能 View 数量从传统方案的 382 个降到个位数
选择模式 单选 / 多选 / 范围选择 / 默认自动选
视图 月视图 / 周视图 / 年视图,支持月↔周收缩切换
特色 农历、节气、节日、热力图、进度条、魅族/iOS 风格皆可定制
版本 免费开源最终版 3.7.1(AndroidX)/ 3.6.8(support)核心要点:
1. 它是"纯 Canvas + 插拔式"的日历,不依赖 RecyclerView/GridView
2. 框架只实现逻辑(翻页、选择、范围、农历计算),不实现 UI
3. UI 由客户端继承 MonthView 用 Canvas 画,自由度无限
4. 支持 4 种选择模式 + 月/周/年三视图 + 可收缩日历
2、为什么快:Canvas vs 传统方案
传统日历卡顿的根因是View 数量爆炸。官方文档算了一笔账,直观看懂 CalendarView 的性能优势从哪来:
// 传统方案(基于 RecyclerView/GridView 的月视图)
ViewPager(1)
+ RecyclerView or GridView(3 个,ViewPager 默认持有 3 页)
+ 每页 42 个 item × 每个 3 个控件(天数+农历+本身) = 3 × 42 × 3 = 378
= 总计约 382 个 View!
// CalendarView 方案
1 个 View + Canvas 直接绘制,View 数量降 360+
// 结论:用 1 个 View + Canvas 代替 382 个 View
// 内存与渲染性能优势"相当明显"(官方原话)核心要点:
1. 传统月视图 = ViewPager + RecyclerView + 42 item × 多控件 ≈ 382 个 View
2. CalendarView 用 1 个 View + Canvas 替代,View 数降一个数量级
3. View 越少 → 测量/布局/绘制开销越低 → 滑动越流畅
4. 这是它能"极速渲染 + 极低内存"的根本原因,也是与同类库的最大区别
3、插拔式设计:核心思想
这是 CalendarView 最有创意的地方。官方用"插座"打比方:插座不管你插灯泡还是风扇,只要符合接口都能用。框架不提供 UI 实现,UI 由客户端通过 Canvas 自定义,既可编译时指定,也可运行时热插拔:
// 插拔式设计:框架只管逻辑,UI 由你用 Canvas 画
// 方式一:编译时指定(XML 属性)
app:month_view="com.xxx.MeiZuMonthView"
app:week_view="com.xxx.MeiZuWeekView"
// 方式二:运行时热插拔(动态换样式)
mCalendarView.setMonthView(MeiZuMonthView.class);
mCalendarView.setWeekView(MeiZuWeekView.class);
// 好处:日历 UI 不再"千篇一律"
// 想要魅族风、iOS 风、热力图、进度条风,换一个 View 类即可核心要点:
1. 框架不画 UI——这是它"千变万化"又能保持性能的根基
2. 编译时用 app:month_view 指定自定义视图类的全限定名
3. 运行时用 setMonthView(class) 热插拔,即插即用、不爽就换
4. 客户端只需"继承 MonthView + 实现 Canvas 回调",坐标框架已算好
4、自定义月视图:三个 Canvas 回调
定制 UI 的入口就是继承 MonthView(或对应模式的基类),实现三个回调:选中态、标记态、文本。框架已把坐标和参数算好传给你,你只管用 Canvas 画:
// 自定义月视图:继承 MonthView,实现三个 Canvas 回调
public class MeiZuMonthView extends MonthView {
// ① 绘制选中的日期
@Override
protected boolean onDrawSelected(Canvas canvas, Calendar calendar,
int x, int y, boolean hasScheme) {
// x、y 是该日期 Card 的起点坐标,已由框架算好
// 返回 true:继续调用 onDrawScheme(背景色不互斥)
return true;
}
// ② 绘制有标记事件(日程)的日期
@Override
protected void onDrawScheme(Canvas canvas, Calendar calendar,
int x, int y) {
// 画一个小圆点、进度环、热力色块等标记
}
// ③ 绘制文本(公历/农历/特殊字符串)
@Override
protected void onDrawText(Canvas canvas, Calendar calendar,
int x, int y, boolean hasScheme, boolean isSelected) {
// 想怎么显示就怎么画——不画就不显示
// 隐藏农历、把某天换成"假"字、特殊日期高亮,都在这里
}
}
// 周视图(WeekView)同理,只是回调里没有 y 参数(单行)核心要点:
1. onDrawSelected:画选中样式(背景圆/色块),返回是否继续画 scheme
2. onDrawScheme:画有日程标记的日期(小圆点/进度环等)
3. onDrawText:画文字,不画就不显示——隐藏农历、换特殊字符都在这
4. 坐标 x/y 框架已算好,无需自己计算布局位置
5、四种选择模式
通过 select_mode 切换选择行为,每种模式对应不同的继承基类。酒店/旅游用范围选择,签到用单选,打卡用多选:
select_mode 继承基类 说明
-----------------------------------------------------------------------
default_mode MonthView / WeekView 默认手机日历:自动选首日,无拦截器
single_mode MonthView / WeekView 单选,支持拦截器(精确控制可选日期)
range_mode RangeMonthView 范围选择(酒店/旅游订房区间)
multi_mode MultiMonthView 多选(签到打卡、多日标记)
// XML 指定选择模式
app:select_mode="single_mode"
app:select_mode="range_mode"
app:select_mode="multi_mode"
// 酒店场景还需限定可选范围(不能订昨天、不能无限期)
app:min_year="2024" app:min_year_month="1" app:min_year_day="1"
app:max_year="2025" app:max_year_month="12" app:max_year_day="31"
// 或运行时:mCalendarView.setRange(minY, minM, minD, maxY, maxM, maxD)核心要点:
1. default_mode:仿系统日历,自动选当月首日,最简单
2. single/range/multi 模式支持拦截器,可精确控制哪些日期可选
3. 范围/多选需换对应基类(RangeMonthView/MultiMonthView),不能混用
4. setRange 可静态或动态限定日期边界,满足"不能订昨天"类业务约束
6、CalendarLayout:可收缩日历
把 CalendarView 套进 CalendarLayout,就能实现"月视图 ↔ 周视图"无缝收缩切换(手势平滑、不抖动)——这是签到/日程类 App 最常见的交互:
<com.haibin.calendarview.CalendarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:default_status="shrink" <!-- 默认收起(周视图) -->
app:calendar_show_mode="only_week_view" <!-- 收起后只显周视图 -->
app:calendar_content_view_id="@+id/recyclerView"> <!-- 内容布局,用于平移过渡 -->
<com.haibin.calendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:month_view="com.xxx.SimpleMonthView"
app:week_view="com.xxx.SimpleWeekView"
app:week_bar_view="com.xxx.EnglishWeekBar"
app:current_month_text_color="#333333"
app:selected_theme_color="#333"
app:week_start_with="mon" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.haibin.calendarview.CalendarLayout>核心要点:
1. CalendarLayout 包裹 CalendarView + 内容区,实现月↔周收缩
2. calendar_content_view_id 必填,指定内容布局用于月视图平移过渡
3. default_status:expand(展开月视图)/ shrink(收起为周视图)
4. setOnViewChangeListener 监听收缩状态变化,联动下方列表
7、集成与常用 API
依赖一行引入(AndroidX 用 3.7.1),再掌握几个核心 API 就能跑起来:
// app/build.gradle(AndroidX)
implementation 'com.haibin:calendarview:3.7.1'
// support 包用 3.6.8:implementation 'com.haibin:calendarview:3.6.8'
// 核心 API
mCalendarView.setOnCalendarSelectListener(new CalendarView.OnCalendarSelectListener() {
@Override public void onCalendarSelect(Calendar calendar, boolean isClick) {
// 日期被选中(点击或滑动翻页)
}
@Override public void onCalendarOutOfRange(Calendar calendar) { }
});
mCalendarView.setOnMonthChangeListener((year, month) -> { /* 月份切换 */ });
mCalendarView.setSchemeDate(map); // 标记有日程的日期(配合 onDrawScheme)
mCalendarView.setWeekStarWithMon(); // 周起始:Mon/Sun/Sat 三选一
mCalendarView.setRange(minY,minM,minD, maxY,maxM,maxD); // 限定可选范围
mCalendarView.scrollToCalendar(y, m, d); // 跳转到指定日期
mCalendarView.scrollToPre(); // 上一月/周
mCalendarView.scrollToNext(); // 下一月/周核心要点:
1. AndroidX 用 3.7.1,support 用 3.6.8,注意版本对应
2. setOnCalendarSelectListener 是最常用回调,处理日期选择
3. setSchemeDate 标记日程,配合自定义 View 的 onDrawScheme 画标记
4. scrollToXXX 系列用于"跳转到某天",可与其他控件联动
8、总结
CalendarView 的两个核心决策——纯 Canvas 换性能、插拔式换自由度——让它在 Android 日历库里独树一帜:既快得离谱(382 View → 1 View),又能画成任何样子。
关键要点:
- 定位:纯 Canvas 绘制的高性能可定制日历库,框架不画 UI
- 性能:用 1 View + Canvas 替代传统 382 个 View,内存与渲染全面占优
- 定制:继承 MonthView 实现 onDrawSelected/Scheme/Text 三个 Canvas 回调
- 模式:default/single/range/multi 四种选择,配合拦截器与 setRange
- 收缩:CalendarLayout 实现月↔周无缝切换,需指定 calendar_content_view_id
- 边界:免费开源最终版 3.7.1;垂直/水平切换、高仿 iOS 日历等已不再开源
- ProGuard:keep 自定义 MonthView/WeekView/YearView/WeekBar 的构造函数
对于做签到、预约、酒店、账单、健身打卡等"带日历"场景的开发者而言,CalendarView"Canvas 性能 + 插拔式 UI"的设计非常值得掌握——它用"框架管逻辑、客户端画 UI"的分工,把性能和自由度这对矛盾一次性化解了。哪怕你只是用它默认样式,纯 Canvas 带来的流畅度也远超基于 RecyclerView 的同类库;而一旦你学会继承 MonthView 写那三个 Canvas 回调,热力图、进度环、魅族风、全屏年历……几乎没有它画不出来的日历。把"382 View 降到 1 个"和"UI 即插即用"这两条设计精髓理解透,你也就抓住了高性能自定义控件的通用方法论。
