kotlin - Android开发之日历篇(1)

  • 背景
    开发过程中,我们经常会遇见使用日历组件,有时候需要我们自己做高度自定义。因此下面我们来讲一下如何使用合适的算法来绘制我们自己的日历组件。

  • 需求分析
    要显示一个日历组件,我们需要知道日历的大致外观是怎么样的?

    日历图片
    日历图片

    从上图中看,
    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定制,特殊计算等。因此熟悉一些公式的使用是很有必要的。还有就是注意观察我们需要实现的东西的细节,选择合适的方法进行分步骤解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容