sxwnl安卓源码:公农历转换、节气与干支算法集成指南
发布时间:2026/10/14 19:36:27 锦皓数字建站

简介一份来自CSDN的寿星万年历安卓端完整源码围绕农历/节气展示这一核心场景演示了Android四大组件中的Activity/Fragment/Service配合使用、ContentProvider与SQLite本地数据读写、系统Calendar API同步、AlarmManager定时提醒及Android 6.0运行时权限等关键机制是Android开发者拆解真实日历类项目的好素材。整个资源包含179个文件以65个Java类、64个XML布局及资源配置、42个PNG图标为主另带SQLite数据库、Android-support-v4.jar等运行与调试基础组件压缩包体积仅2.34MB结构清晰、便于按模块分析。源码内置农历转换与节气推算算法、自定义MonthView日历网格、SlidingMenu侧滑菜单、WheelView滚动选择器及借款计算器等现成模块可直观学习滚动列表适配、自定义View绘制、动态布局加载和事件分发等进阶写法。已有930人浏览学习适合希望从零梳理Android数据层、UI层与系统服务协同的开发者参考实践。1. 安卓应用要做农历、节气、干支最省心的是直接拿一套成熟源码来改做过日历类应用的开发者都有同感公历转农历看似简单真要把闰月、节气时刻、干支纪年全部算对自己从头写至少要多花两三周而且很容易在边界日期上翻车。寿星万年历sxwnl安卓源码就是为这个场景准备的——它把公农历转换、节气计算、干支生肖、闰月判断这些硬骨头算法做成了可直接调用的安卓工程打开就能跑核心逻辑是纯Java实现不依赖服务端。它的价值在于你不需要懂天文算法细节只需要搞清楚怎么调用、怎么改界面、怎么处理边界就能把一个能用的万年历功能嵌进自己的App里。这套源码适合三类人一是产品里需要“农历日期节气提醒”但不想从零写算法的客户端工程师二是想研究公农历换算实现的学生或入门开发者三是接外包单子需要快速交付日历模块的开发者。接下来从源码结构、核心调用、集成步骤、经典踩坑、验证方法到进阶技巧一条线讲完。2. sxwnl 源码第一眼从类结构看懂万年历的换算主线2.1 目录与核心类谁是入口、谁是算法、谁是展示拿到这份安卓源码后先别急着跑。我习惯先打开工程目录把包结构扫一遍弄清哪些是算法核心、哪些只是界面壳子。sxwnl 源码的典型结构大致是这样src/main/java/com/demo/calendar/ ├── core/ │ ├── sxwnl/ │ │ ├── Solar.java // 公历对象 │ │ ├── Lunar.java // 农历对象 │ │ ├── LunarSolarConverter.java // 公农历互转核心 │ │ ├── SolarTermsUtil.java // 节气计算 │ │ └── GanZhiUtil.java // 干支纪年纪日 ├── ui/ │ ├── CalendarActivity.java // 日历页入口 │ └── MonthAdapter.java // 网格适配器 └── util/ ├── DateConvertManager.java // 转换管理封装 └── FestivalUtil.java // 节日、节气标记这里每个类的职责很清晰。Solar和Lunar是两个基础数据对象一个装载公历年月日时分秒一个装载农历年月日、是否闰月、干支、生肖等字段。LunarSolarConverter是核心入口所有公农历互转都走它。SolarTermsUtil负责节气计算它返回的是某个节气在公历里的精确时刻不是近似日期。GanZhiUtil处理干支和生肖这里有一个容易搞错的地方干支和生肖以立春为界而不是以正月初一为界这个后面避坑章节再展开。从使用角度讲你主要打交道的类是DateConvertManager这个封装类里面维护了缓存和线程安全逻辑。最底层的SolarTermsUtil和GanZhiUtil一般情况下不需要直接动除非你要修改节气的判定口径。2.2 公历转农历与农历转公历两条主链路的调用方式万年历的核心功能翻来覆去就是那两件事公历转农历得到农历日期农历转公历得到对应的公历日期。sxwnl 的入口方法非常直白看一眼就能用// 公历 2024年2月10日 12点0分0秒 Solar solar new Solar(2024, 2, 10, 12, 0, 0); Lunar lunar LunarSolarConverter.SolarToLunar(solar); Log.d(sxwnl, 农历 lunar.getLunarYear() 年 (lunar.isLeap() ? 闰 : ) lunar.getLunarMonth() 月 lunar.getLunarDay() 日); Log.d(sxwnl, 干支 lunar.getYearGanZhi() 生肖 lunar.getAnimal());这段代码的逻辑是先构造一个公历对象把年月日时分秒传进去然后调用SolarToLunar静态方法拿到一个Lunar对象。getLunarYear()拿农历年isLeap()判断这个月是否是闰月getLunarMonth()和getLunarDay()拿农历月和日getYearGanZhi()拿干支getAnimal()拿生肖。参数方面要注意Solar构造函数里的时分秒不是摆设节气计算依赖精确时间如果你只需要日期传 0 点 0 分 0 秒也能算出正确农历日期但算节气会有偏差。反向转换同样简单Lunar lunar new Lunar(2024, 1, 1, false); // 2024年正月初一非闰月 Solar solar LunarSolarConverter.LunarToSolar(lunar); Log.d(sxwnl, 公历 solar.getSolarYear() 年 solar.getSolarMonth() 月 solar.getSolarDay() 日);Lunar 的构造函数参数依次是农历年、农历月、农历日、是否闰月。这里有个使用习惯要养成先确认目标年份的农历年是否正确比如公历2024年2月10日对应的农历年是甲辰年但农历年号是 2024不要被公历年误导。2.3 节气与干支纪年不是查表是算出来的很多轻量级日历库的节气是硬编码查表每年手动维护一张表一过范围就出错。sxwnl 的节气计算走的是天文算法路线根据年份计算太阳黄经到达特定度数的时刻然后把时刻换算成公历日期时间。节气数据的获取方式如下SolarTerm term SolarTermsUtil.getSolarTerm(2024, 4); // 第4个节气清明 Log.d(sxwnl, 清明时刻 term.getSolar().getSolarYear() - term.getSolar().getSolarMonth() - term.getSolar().getSolarDay() term.getSolar().getSolarHour() : term.getSolar().getSolarMinute());第一个参数是年份第二个是节气序号从 1 到 24。这里的序号不是月份而是节气在一年里的排列顺序比如 1 是小寒2 是大寒3 是立春4 是清明。看到这个调用方式时要知道节气计算依赖整点时刻算法内部对 1900 年之后的数据有较高精度1900 年以前会退化所以设计产品时别把年份范围拉得太宽。干支纪年的算法更有意思。它把年份拆成天干地支两个维度循环60 年一轮回。sxwnl 在计算干支时以立春时刻作为年份分界点——这意味着同一个公历日期在立春前和立春后干支和生肖可能完全不同。3. 把 sxwnl 集成进你的安卓工程Gradle 配置与工具类封装3.1 引入方式模块拷贝还是源码依赖sxwnl 源码的集成有两种方式我自己两种都用过做出的选择很明确直接把 core 包拷贝进自己的工程而不是把整个 demo 工程当作 module 依赖。理由有三个。第一demo 工程里的 UI 代码和你自己的视觉风格大概率冲突改起来成本比拷贝源码高第二算法包的依赖只有 Java 标准库没有第三方库拷贝后几乎不需要额外配置第三通用功能层面的定制缓存、线程、日志必须改动源码拷贝到自己工程里改着顺手。如果你用 Gradle 管理依赖只在build.gradle里做最小配置android { compileSdk 33 defaultConfig { minSdk 21 targetSdk 33 } } dependencies { // sxwnl 算法包无第三方依赖不需要额外引入 }注意minSdk建议不低于 21这部分考虑不是算法本身的要求而是后面封装用的ConcurrentHashMap、java.time相关类在低版本上有兼容问题。如果你要适配更老的机型需要自己再做兼容处理。3.2 封装一个 DateConvertManager线程安全与结果缓存sxwnl 的核心转换方法都是静态调用本身没有状态意味着线程安全没问题。但实际业务里有个隐患如果不加缓存每次在 UI 层调SolarToLunar都会重新做节气计算和干支推算一个屏幕上几十个日期格子同时刷新卡顿是必然的。所以我建议做一层封装把计算结果缓存起来public class DateConvertManager { private static final int MAX_CACHE_SIZE 1024; private final MapString, Lunar cache new LinkedHashMapString, Lunar(MAX_CACHE_SIZE, 0.75f, true) { Override protected boolean removeEldestEntry(Map.EntryString, Lunar eldest) { return size() MAX_CACHE_SIZE; } }; public Lunar solarToLunar(int year, int month, int day) { String key year - month - day; Lunar cached cache.get(key); if (cached ! null) { return cached; } Solar solar new Solar(year, month, day, 0, 0, 0); Lunar lunar LunarSolarConverter.SolarToLunar(solar); cache.put(key, lunar); return lunar; } }这个封装的思路是用LinkedHashMap实现一个固定大小的 LRU 缓存最多缓存 1024 条转换结果超出后自动淘汰最久没用的记录。key 用公历日期的字符串value 是转换后的Lunar对象。这样同一页面重复渲染时直接从缓存取绕开底层算法。参数方面MAX_CACHE_SIZE 1024对日历场景够用因为一屏最多显示几十天用户翻来翻去也就几百天。如果你要显示十年日历可以调大这个值但要注意内存占用每个Lunar对象里有多个字符串字段1024 条大约占几百 KB 内存。3.3 在 RecyclerView 里按周翻页从日期到农历的最后一公里集成到列表里时不要在onBindViewHolder里直接调算法。我见过太多人把转换写进 UI 绑定流程最后滑动卡成 PPT。正确做法是提前算好一屏加一页的缓存然后在绑定阶段只读缓存。public class MonthAdapter extends RecyclerView.AdapterMonthAdapter.VH { private final DateConvertManager convertManager new DateConvertManager(); private ListDayCell displayDays new ArrayList(); static class DayCell { int solarYear; int solarMonth; int solarDay; String lunarText; boolean isFestival; } Override public void onBindViewHolder(VH holder, int position) { DayCell cell displayDays.get(position); holder.solarText.setText(cell.solarYear - cell.solarMonth - cell.solarDay); holder.lunarText.setText(cell.lunarText); if (cell.isFestival) { holder.lunarText.setTextColor(Color.parseColor(#D32F2F)); } } public void updateDays(int year, int month) { // 预计算传入月首日和月尾日生成 DayCell 列表 displayDays buildMonthCells(year, month); notifyDataSetChanged(); } private ListDayCell buildMonthCells(int year, int month) { ListDayCell cells new ArrayList(); // 循环当月每一天通过 convertManager.solarToLunar 得到农历字段 // 组装 lunarText 和 isFestival 后放入 cells return cells; } }这里的核心思想是UI 绑定只做赋值操作不做任何计算逻辑。buildMonthCells在数据更新时一次性生成整个月的DayCell列表之后onBindViewHolder只是把提前算好的字符串往控件上填。if (cell.isFestival)用于高亮传统节日比如春节、中秋这类信息也是预先在DayCell里标记好的。4. 避坑专用章sxwnl 安卓集成中的五个经典翻车现场4.1 节气时刻差了 8 小时对不上权威日历现象代码里算出清明是 15:02但用户手机显示 16:02怎么看都差一个小时。原因sxwnl 的节气算法返回的是东八区北京时间而系统Calendar默认使用设备本地时区。如果设备在国外时区或者你用了UTC格式化就会出现整小时偏移。解决输出节气时间时强制指定时区不要依赖系统默认值。SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm, Locale.getDefault()); sdf.setTimeZone(TimeZone.getTimeZone(GMT08:00)); String text sdf.format(termTimeMillis);从那以后我遇到日历类需求所有时间格式化都统一写一个工具方法强制走东八区避免各写各的。4.2 闰月显示成“下个月”月份序号大了一现象2020 年有闰四月公历 5 月 23 日起进入闰四月。界面上却显示成“五月”导致整个月的农历日期全部错位。原因拿到Lunar对象后直接用getLunarMonth()当作显示月份没有先判断isLeap()。闰四月和五月是相邻的两个月但月份序号都是 4只是闰月标志不同。解决在封装层就把显示文本拼好避免 UI 层自己判断if (lunar.isLeap()) { lunarText 闰 lunar.getLunarMonth() 月; } else { lunarText lunar.getLunarMonth() 月; }做数据绑定的时候永远只信任封装层给出的字符串不要在onBindViewHolder里做逻辑判断。4.3 生肖在正月初一之后变了但有人说是“立春才换”现象App 在 2024 年 2 月 10 日正月初一显示“龙”而另一款日历在 2 月 4 日立春就显示“龙”两边对不上。原因sxwnl 的getAnimal()按农历年计算正月初一为界但部分民俗场景按立春为界。两种说法都有依据关键是产品口径要统一不要混用。解决在需求阶段明确“生肖/干支按农历年还是立春切换”。如果产品需要立春口径不要改Lunar.getAnimal()而是单独用GanZhiUtil按立春时刻计算。这属于定制需求文档里要写清楚避免上线后扯皮。4.4 快速滑动列表时 ANR“卡死”在某个日期转换现象从一月快速滑到十二月界面卡住几秒随后弹出“无响应”对话框。原因RecyclerView的快速滚动会触发大量onBindViewHolder调用而每个绑定里都执行了一次SolarToLunar。节气计算的三角函数耗时不低加上SimpleDateFormat创建对象的开销主线程直接被打垮。解决严格执行前面说的“预计算 缓存”方案并且在DateConvertManager里把缓存改成线程安全版本MapString, Lunar cache new ConcurrentHashMap();如果数据量特别大还可以加一个异步填充逻辑先显示公历日期等算完再notifyItemRangeChanged补上农历。4.5 年份超过 1900 年范围数据直接错乱现象输入 1899 年 12 月 31 日农历显示成 1900 年冬月三十看起来合理但实际是错的。原因sxwnl 的农历数据表只覆盖 1900 年之后更早年份没有数据支撑算法只能硬凑。解决对外接口做范围校验if (year 1900 || year 2100) { throw new IllegalArgumentException(sxwnl 仅支持 1900-2100 年); }产品上线前把输入框的日期范围限制死不要给用户留出错空间。5. 验证与压测万年历不是“能跑”就行得对得上日子5.1 用五十个已知日期做回归对照集成完成之后第一件事不是看界面而是做数据验证。我会准备一批“已知正确答案”的日期对写一个冒烟测试跑一遍确保算法在主干路上没有偏差。下面是一组常用对照数据可以直接用公历日期期望农历期望干支/生肖2024-02-10甲辰年正月初一甲辰 龙2023-01-22癸卯年正月初一癸卯 兔2020-01-25庚子年正月初一庚子 鼠2020-05-23闰四月初一庚子 鼠2000-01-01己卯年冬月廿五己卯 兔写测试代码时可以这样组织Test public void testSolarToLunar() { Lunar lunar DateConvertManager.getInstance() .solarToLunar(2024, 2, 10); assertEquals(正月初一, lunar.getLunarMonth() 月 lunar.getLunarDay() 日); assertEquals(甲辰, lunar.getYearGanZhi()); assertEquals(龙, lunar.getAnimal()); }这段测试的最佳实践是把对照表做成 CSV 或 JSON 放assets目录测试代码循环读取每读一行验证一行。以后改算法、改封装跑一遍这个脚本就知道有没有改坏。我在项目里维护着一个 60 行的对照表覆盖闰月、春节、除夕、腊月、元旦这些容易出错的边界。5.2 边界测试闰月、年首年尾、极早极晚日期边界测试要专门做一组因为农历算法的 bug 大多藏在日期的极限位置。我一般会在测试代码里穷举“每月最后一天”和“每年最后一天”// 检查一年内所有日期的转换不抛异常且结果有值 Test public void testEveryDayInOneYear() { DateConvertManager manager DateConvertManager.getInstance(); int year 2024; Calendar calendar Calendar.getInstance(); calendar.clear(); calendar.set(year, 0, 1); for (int i 0; i 366; i) { int y calendar.get(Calendar.YEAR); int m calendar.get(Calendar.MONTH) 1; int d calendar.get(Calendar.DAY_OF_MONTH); Lunar lunar manager.solarToLunar(y, m, d); assertNotNull(lunar); assertTrue(lunar.getLunarYear() 1900); assertTrue(lunar.getLunarMonth() 1 lunar.getLunarMonth() 12); calendar.add(Calendar.DAY_OF_MONTH, 1); } }这种“扫一年”的测试看起来简单但能抓出大量问题某一天对应的农历月超出 1-12 范围、某一天转换结果为空、某个月份缺失。另外 2033 年前后是农历算法的经典困难年份建议单独把 2033 年的全年扫一遍。5.3 性能压测转换 10 万次日历的耗时与内存万年历的算法是纯 CPU 计算性能指标主要看两点单次转换耗时、高并发下的内存增长。压测代码长这样Test public void testPerformance() { DateConvertManager manager DateConvertManager.getInstance(); long start System.nanoTime(); for (int i 0; i 100000; i) { int year 1900 (i % 200); int month 1 (i % 12); int day 1 (i % 28); manager.solarToLunar(year, month, day); } long cost System.nanoTime() - start; float avgMs cost / 100000f / 1_000_000f; Log.d(sxwnl, 平均单次转换耗时: avgMs ms); }在我的测试环境里带缓存的DateConvertManager平均单次耗时在 0.01ms 级别而不带缓存直接调LunarSolarConverter大约在 0.1-0.5ms。差距看起来不大但一屏 42 个格子一页就有 4-20ms足以让列表滑动掉帧。性能目标定一个数单次转换不超过 1ms内存增长不超过 5MB/10万次超过就要查缓存是不是没生效。6. 最后一个技巧把节气做成“当天提醒”比满屏红字好用很多日历 App 的节气展示就是一行灰色小字用户根本注意不到。我建议把“节气当天提醒”做成系统通知这样能提升活跃度又不需要额外权限。核心实现是用 sxwnl 算出下一个节气日期然后在当天上午 9 点发一条通知。public void scheduleNextTerm(Context context) { Lunar lunar DateConvertManager.getInstance().solarToLunar( currentYear, currentMonth, currentDay); // 从当前日期顺延查最近的一个节气 SolarTerm nextTerm SolarTermsUtil.getNextSolarTerm( currentYear, currentMonth, currentDay); // 构造提醒时间节气当天上午 9:00 Calendar remindTime Calendar.getInstance(); remindTime.set(nextTerm.getSolar().getSolarYear(), nextTerm.getSolar().getSolarMonth() - 1, nextTerm.getSolar().getSolarDay(), 9, 0, 0); AlarmManager alarmManager (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent intent new Intent(context, TermRemindReceiver.class); intent.putExtra(term_name, nextTerm.getName()); PendingIntent pendingIntent PendingIntent.getBroadcast(context, nextTerm.getSolar().getSolarDay(), intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); alarmManager.setExact(AlarmManager.RTC_WAKEUP, remindTime.getTimeInMillis(), pendingIntent); }这里有两个细节要留意。第一AlarmManager.setExact在部分国产 Rom 上会被系统限频导致通知延迟到达需要引导用户把应用加入“自动启动”白名单。第二PendingIntent的 requestCode 我直接用日期里的“日”字段避免同一天多个节气互相覆盖。我从一开始做日历应用时总是喜欢把所有日期都算好再展示结果上了线才发现老机型卡顿、时区偏移、闰月错位这些事。从那以后我每次集成这类日历算法都强制自己走一遍“验证脚本 边界测试 压测”三部曲再顺手把节气提醒加好产品完整度立刻高了一个档次。这份源码能直接解决农历算法这块硬骨头剩下的就是你在 UI 和提醒这些地方多花心思。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。