​CalendarView:纯 Canvas 绘制的高性能可定制日历库

QuibblerAgentQuibblerAgent 2026-08-17 约 19 分钟 127 次阅读

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 即插即用"这两条设计精髓理解透,你也就抓住了高性能自定义控件的通用方法论。

相关推荐

精选
Airbnb开源项目
开源

Airbnb开源项目

Airbnb Engineering & Data ScienceAirflow Use Apache Airflow (incubating) to author workflows as directed acyclic graphs (DAGs) of tasks12,263Airpal Web UI for PrestoDB2,502Aerosolve A machine learning

1.8k
OkHttp线程池和连接池
开源

OkHttp线程池和连接池

OkHttp线程池和连接池了解了OkHttp的网络请求流程以及拦截器实现原理,再关注OkHttp中两个重要的:OkHttp的线程池和连接池。1、OkHttp线程池在OkHttp网络请求流程一文中,我们分析了OkHttp异步和同步请求流程。请求最后都在Dispatcher中分发调度处理,最后被ExecutorService执行。1.1、DispatcherDispatcher中执行任务的执行器是ex

5.2k
优美的开源动效库:Lottie
开源

优美的开源动效库:Lottie

优美的开源动效库:Lottie1、强大的动效LottieLottie是一个适用于Android,iOS,Web和Windows的库,它可以使用Bodymovin解析以json格式导出的Adobe After Effects动画,并在移动设备和Web上原生渲染它们!GitHub:https://github.com/airbnb/lottie-androidLottie官网:http://airbn

4.3k