背景
开发过程中,我们经常会遇见使用日历组件,有时候需要我们自己做高度自定义。因此下面我们来讲一下如何使用合适的算法来绘制我们自己的日历组件。-
需求分析
要显示一个日历组件,我们需要知道日历的大致外观是怎么样的?
从上图中看,
a. 日历大致有表头,星期显示,日期显示以及农历(此处暂不讨论)显示。
b. 日期格子占有的数量是:6 × 7 = 42 个,其中包含有:上月的部分日期,本月的全部日期以及下月的部分日期。明白一点说就是,如果我们想计算某个月的日历,那么我们需要知道本月的一号是星期几,有多少天,上个月在这个月的日历中占有几天以及下个月在这个月的日历中占有几天,那么我们就可以计算出当月的日历数据了。 具体实现
由于年份中存在闰年和平年的概念,并且影响2月的天数,因此我们需要一个判断闰年和平年的方法,如下:
/**
* 判断某一年是否是闰年
* @param year
*/
@JvmStatic
fun isLeapYear(year: Int): Boolean = year % 4 == 0 && year % 100 != 0 || year % 400 == 0
计算每个月的1号是星期几?查看网上资料
关于这个星期几的计算,有多种计算公式。
①. 常用公式
W = [Y-1] + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] + D
Y是年份数,D是这一天在这一年中的累积天数,也就是这一天在这一年中是第几天。
②. 蔡勒(Zeller)公式
w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1
公式中的符号含义如下,w:星期;c:世纪;y:年(两位数);m:月(m大于等于3,小于等于14,即在蔡勒公式中,某年的1、2月要看作上一年的13、14月来计 算,比如2003年1月1日要看作2002年的13月1日来计算);d:日;[ ]代表取整,即只要整数部分。相比于通用通用计算公式而言,蔡勒(Zeller)公式大大降低了计算的复杂度。
③. 对蔡勒(Zeller)公式的改进
相比于另外一个通用通用计算公式而言,蔡勒(Zeller)公式大大降低了计算的复杂度。不过,笔者给出的通用计算公式似乎更加简洁(包括运算过程)。现将公式列于其下:
***W=[y/4]+r (y/7)-2r(c/4)+m’+d***
公式中的符号含义如下,r ( )代表取余,即只要余数部分;m’是m的修正数,现给出1至12月的修正数1’至12’如下:(1’,10’)=6;(2’,3’,11’)=2;(4’,7’)=5;5’=0;6’=3;8’=1;(9’,12’)=4(注意:在笔者给出的公式中,y为润年时1’=5;2’=1)。其他符号与蔡勒(Zeller)公式中的含义相同。
④. 基姆拉尔森计算公式
W= (d+2m+3(m+1)/5+y+y/4-y/100+y/400) mod 7
在公式中d表示日期中的日数,m表示月份数,y表示年数。
注意:在公式中有个与其他公式不同的地方:
把一月和二月看成是上一年的十三月和十四月,例:如果是2004-1-10则换算成:2003-13-10来代入公式计算。
此处,我们取第④种公式,因此有如下代码:
/**
* 计算星期几? 取值范围从 1 - 7
*
* @param year 当前年份
* @param month 当前月份
* @param date 当前日期
* @return
*/
@JvmStatic
fun computeWeekNum(year: Int, month: Int, date: Int): Int {
val isMonth1Or2 = month == 1 || month == 2
val month1 = if (isMonth1Or2) month + 12 else month
val year1 = if (isMonth1Or2) year - 1 else year
val weekNum = (date + 2 * month1 + 3 * (month1 + 1) / 5 + year1 - year1 / 100 + year1 / 4 + year1 / 400) % 7
return weekNum + 1
}
在日历计算中,我们还需要知道当月天数,前一个月的天数以及后一个月的天数,所以:
/**
* 计算月份的天数
*
* @param year 当前年份
* @param month 当前月份
* @return
*/
@JvmStatic
fun computeMonthDayCount(year: Int, month: Int): Int {
when (month) {
2 -> return if(isLeapYear(year)) 29 else 28
1, 3, 5, 7, 8, 10, 12 -> return 31
4, 6, 9, 11 -> return 30
}
return -1
}
磨刀不误砍柴工,现在我们的准备工作都已经完成,现在就要开始日历的计算工作了。
/**
* 计算日历中的日期数据,其中涉及到的数字42就是6×7的布局。
* @param year 年份
* @param month 月份
*/
@JvmStatic
fun computeDatesInCalendar(year: Int, month: Int): MutableList<Int> {
val dates: MutableList<Int> = mutableListOf()
val monthDayCount = computeMonthDayCount(year, month) //计算出当前这个月有多少天
val preMonthDayNum = computeWeekNum(year, month, 1) //先获得该日期下是周几?然后计算出上个月有几天要在日期中显示
val preMonthDayCount = computeMonthDayCount(if (month == 1) year - 1 else year, if (month == 1) 12 else month - 1)
val nextMonthDayNum = 42 - preMonthDayNum - monthDayCount //计算出下一个月要显示几天
IntRange(0, preMonthDayNum - 1).forEach { dates.add(preMonthDayCount - (preMonthDayNum - 1 - it)) } //填充上个月要显示的天数
IntRange(0, monthDayCount - 1).forEach { dates.add(it + 1) } //填充本月要显示的天数
IntRange(0, nextMonthDayNum - 1).forEach { dates.add(it + 1) } //填充下一月要显示的天数
return dates
}
现在我们来测试一下结果,如下:
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
List<Integer> data = DateUtil.computeDatesInCalendar(2017, 1);
for (int i = 0; i < data.size(); i++) {
System.out.print(data.get(i) + "\t");
if ((i + 1) % 7 == 0) {
System.out.print("\n");
}
}
assertEquals(4, 2 + 2);
}
}
结果输出:
25 26 27 28 29 30 31
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 1 2 3 4
结果我们已经算出来了,现在造UI轮子还难吗?下面是一个Git地址,存了使用该算法的一个demo,只是其中涉及到的公式选用的蔡勒(Zeller)公式。
Git项目地址(项目使用java代码编写): http://git.oschina.net/yugecse/CalendarWidget
- 项目总结:
工作中我们避免不了跟一定的数学公式打交道,尤其是一些UI定制,特殊计算等。因此熟悉一些公式的使用是很有必要的。还有就是注意观察我们需要实现的东西的细节,选择合适的方法进行分步骤解决。