先看几张动态的效果图吧!

这里主要记录一下在编写日历控件过程中一些主要的点:

一、主要功能

1、支持农历、节气、常用节假日

2、日期范围设置,默认支持的最大日期范围[1900.1~2049.12]

3、禁用日期范围设置

4、初始化选中单个或多个日期

5、单选、多选操作

6、跳转到指定日期

7、替换农历为指定文字

8、通过自定义属性定制日期外观,以及简单的日期item布局配置

9、......

二、基本结构

我们要实现的日历控件采用ViewPager作为主框架,CalendarView继承ViewPager,这样就天生拥有左右滑动和缓存的功能。目前我们设定日历左右滑动为月份切换的操作,每一个月份显示通过自定义ViewGroup实现,也就是我们的MonthView,月份中的日期是通过layout布局解析出的View,根据月份的不同每个MonthView可能包含6 x 7或5 x 7个日期View,由于给ViewPager绑定数据需要通过PagerAdapter,所以继承PagerAdapter我们扩展了一个CalendarPagerAdapter,来完成MonthView的相关初始化和日期数据的绑定。

三、计算每个MonthView需要填充的日期数据

从上边的截图可以看出,每个MonthView的日期数据应该由上个月的后0~6天、当前月的天数和下个月的前0~6天组成。首先计算出当前月有多少天,这个简单,以及根据年月算出当前月的第一天是星期几:

public static int getFirstWeekOfMonth(int year, int month) {

Calendar calendar = Calendar.getInstance();

calendar.set(year, month, 1);

return calendar.get(Calendar.DAY_OF_WEEK) - 1;

}

返回0代表周日,1~6代表周一到周六,以上边的截图为例,可以知道2017年5月的第一天是周一:week = getFirstWeekOfMonth(2017, 5-1),按照如下伪码则可计算出包含的上个月的日期:

for (int i = 0; i < week; i++) {

ld = 上个月天数 - week + 1 + i;

}

至于包含的下个月的日期和当前MonthView显示的行数有关,如果 当前月的天数+week可以被7整除则不需要包含下月日期,否则需要计算包含的下月日期,伪码如下:

for (int i = 0; i < 7 * 显示的行数 - 当月天数 - week; i++) {

nd = i + 1;

}

这样需要的日期数据就计算完了,详细的算法可参考源码。

四、 计算日历的总页数

总页数应由日历的起始年月得到,其实就是确定ViewPager的总页数,这样好理解点。可按照如下方法计算:

count = (dateEnd[0] - dateStart[0]) * 12 + dateEnd[1] - dateStart[1] + 1

其中dateStart、dateEnd是包含日历开始年月和结束年月的数组。这个count也是CalendarPagerAdapter必须的。

五、用position计算日期

PagerAdapter有个instantiateItem()方法:

public Object instantiateItem(ViewGroup container, int position) {

return instantiateItem((View) container, position);

}

来创建ViewPager的每一页,所以日历每一页也是在这里创建的,也就是MonthView,这里有个关键的点就是根据 positon 参数推算出日历每一页对应的年月,然后通过年月计算出当前MonthView需要的日期数据。如何根据position推算出年月呢?

public static int[] positionToDate(int position, int startY, int startM) {

int year = position / 12 + startY;

int month = position % 12 + startM;

if (month > 12) {

month = month % 12;

year = year + 1;

}

return new int[]{year, month};

}

其中startY、startM代表日历的其实年月。有了对应的年月就可以用第二点中的方式计算日期数据,然后填充到MothView中。

六、MothView

前边已经提到了,MonthView继承ViewGroup,也就是日历的每一页,接收到日期数据后,在MonthView中根据数据构造对应的日期View,然后添加View到MonthView中,最后通过onMeasure、onLayout确定每个View最终大小和位置。到这里运行一个ViewPager的基本条件就满足了,在上边提到的instantiateItem()方法中完成MothView的初始化:

public Object instantiateItem(ViewGroup container, int position) {

MonthView view = new MonthView(container.getContext());

//根据position计算对应年、月

int[] date = CalendarUtil.positionToDate(position, dateStart[0], dateStart[1]);

view.setDateList(CalendarUtil.getMonthDate(date[0], date[1]), SolarUtil.getMonthDays(date[0], date[1]));

container.addView(view);

return view;

}

这里只保留了核心的代码,当日历切换月份时,会自动根据position计算出对应月份的日期数据,然后传给MonthView,最后将MonthView添加到ViewPager中。

七、切换月份选中日期

按照目前的设定,当选择当前月的某天后,然后切换月份,新的月份中会找到上次选中的日期,并标记为选中状态,如果找不到则选中新月份的最后一天。其实逻辑很简单,关键是如何在新月份中找到相应的日期并选中。首先记录上次选中的日期,由于ViewPager默认会缓存两页,再加上当前页共三页,在CalendarPagerAdapter中根据position保存三页缓存,当ViewPager切换到某一页后会执行如下回调:

addOnPageChangeListener(new SimpleOnPageChangeListener() {

@Override

public void onPageSelected(int position) {

}

});

在onPageSelected(int position)方法中通过position从缓存中拿到对应的MonthView,也是是切换到的页,这样就能在MonthView中根据记录的日期找到对应的子日期View,然后更改为选中状态。

八、多选

一个理想的多选功能应该是在当前月份选中多个日期后,切换到其它月份,之后回到有选中日期的月份依然能够标记出选中的日期,因为ViewPager有默认的三页缓存,所以在当前月份切换到上月或下月不会有什么问题,但如果切换到前几个月或后几个月,再回到有选中日期的月份,由于之前缓存的页面已经被销毁重建,所以选中的月份也就看不到了。我们的日期点击事件在MonthView中,当每次点击选中时我们需要记录对应年月选中的日期,取消选中时要从记录中删除对应日期,怎么保存呢?在CalendarView类中我们定义一个SparseArray

SparseArray> chooseDate = new SparseArray<>()

其中的HashSet就是指定年月选中的日期,按照我们的规则设定不同年月转换得到的position是唯一对应的,所以我们用position作为SparseArray的key,最后在CalendarView中接收选中或取消选中的操作:

public void setChooseDate(int day, boolean flag, int position) {

if (position == -1) {

position = currentPosition;

}

HashSet days = chooseDate.get(position);

if (flag) {

if (days == null) {

days = new HashSet<>();

chooseDate.put(position, days);

}

days.add(day);

positions.add(position);

} else {

days.remove(day);

}

}

之后就是在月份切换过程中,根据保存的日期数据刷新对应的MonthView,实现选中状态的恢复,这个和第六点类似。

九、跳转到指定日期

要跳转到指定日期,首先要根据日期的年月计算出目标MonthView在日历中的position:

public static int dateToPosition(int year, int month, int startY, int startM) {

return (year - startY) * 12 + month - startM;

}

ViewPager有一个setCurrentItem(int item, boolean smoothScroll)方法,这样就能跳转到position对应的MonthView,然后结合第六点的方法选中对应的日期View。这样跳转到日历设定日期范围内的任意一天都是没问题的。

十、自定义日历样式

CalendarView提供的自定义属性如下:

属性名

格式

描述

默认值

choose_type

enum

设置单选(single)、多选(multi)

single

show_lunar

boolean

是否显示农历

true

show_last_next

boolean

是否在MonthView显示上月和下月日期

true

show_holiday

boolean

是否显示节假日

true

show_term

boolean

是否显示节气

true

switch_choose

boolean

单选时切换月份,是否选中上次的日期

true

solar_color

color

阳历日期的颜色

solar_size

integer

阳历的日期尺寸

14

lunar_color

color

农历的日期颜色

lunar_size

integer

农历的日期尺寸

8

holiday_color

color

节假日、节气的颜色

choose_color

color

选中的日期颜色

day_bg

reference

选中的日期背景(图片)

CalendarView相关方法:

方法名

描述

setInitDate(String date)

设置日历的初始显示年月

setStartEndDate(String startDate, String endDate)

设置日历开始、结束年月

setDisableStartEndDate(String startDate, String endDate)

设置日历的禁用日期范围(小于startDate、大于endDate禁用)

setSpecifyMap(HashMap map)

将显示农历的区域替换成指定文字

setSingleDate(String date)

设置单选时初始选中的日期(不设置则不默认选中)

getSingleDate()

得到单选时选中的日期

setMultiDate(List dates)

设置多选时默认选中的日期集合

getMultiDate()

得到多选时选中的全部日期

toSpecifyDate(int year, int month, int day)

单选时跳转到指定年月日

setOnCalendarViewAdapter(int layoutId, CalendarViewAdapter adapter)

设置自定义日期item样式

init()

日期初始化(以上属性配置完后调用)

setOnPagerChangeListener(OnPagerChangeListener listener)

设置月份切换回调

setOnSingleChooseListener(OnSingleChooseListener listener)

设置单选回调

setOnMultiChooseListener(OnMultiChooseListener listener)

设置多选回调

today()

单选时跳转到今天

nextMonth()

跳转到下个月

lastMonth()

跳转到上个月

nextYear()

跳转到下一年的当前月

lastYear()

跳转到上一年的当前月

toStart()

跳转到日历的开始年月

toEnd()

跳转到日历的结束年月

CalendarUtil.getCurrentDate()

获得当前日期(今天)

默认的日期布局是阳历、阴历垂直排列,节假日会覆盖在农历上显示,这个从上边的静态截图可以看出。如果要使用其它的排列方式,例如水平排列等,就需要提供一个自定的layout(但目前只支持两个TextView显示)。例如:

calendarView.setOnCalendarViewAdapter(R.layout.item_layout, new CalendarViewAdapter() {

@Override

public TextView[] convertView(View view, DateBean date) {

TextView solarDay = (TextView) view.findViewById(R.id.solar_day);

TextView lunarDay = (TextView) view.findViewById(R.id.lunar_day);

return new TextView[]{solarDay, lunarDay};

}

});

给CalendarView绑定一个接口,传入lauoyt,然后返回一个代表阳历和农历的TextView数组。

十一、WeekView

我们将日期和星期的显示功能分割开了,所以CalendarView并不负责星期的显示,

WeekView是星期显示的自定义View,从周日开始依次是周一到周六,可通过自定义属性来配置星期的显示文字,以及文字的颜色、尺寸,这个还是相对简单,具体可见Github中的使用介绍。

十二、小结

这里我们只介绍了日历的基本实现原理,和一些关键的点,其实这种实现方式相对还是比较简单的,容易理解,当然难免有不足的地方,后边根据需要再逐步完善和扩展吧。尽管Github上有许多现成的Calendar,但自己动手实现一个还是收获满满,一个看起来简单的东西,只有亲自尝试了才能体会到其中的滋味,最后希望对大家有所帮助吧!

自定义日历控android,Android 一个日历控件的实现小记相关推荐

  1. 根据瘦子的日历,改装了一个月历控件

    脚本下载   1using System;   2using System.Web;   3using System.Web.UI;   4using System.Web.UI.WebControl ...

  2. Android向系统日历添加事件提醒

    项目场景: 在项目开发过程中,需要使用系统日历来辅助提醒.通过向系统日历中写入事件.设置提醒方式,实现到达某个特定的时间自动提醒的功能 解决方案: 1. 请求权限 //Android6.0以上需要动态 ...

  3. java怎么写桌面日历_Win7桌面怎么添加日历记事本

    可以按照日历记事的软件,电脑和手机都可以使用的,且能实现多端同步,推荐你可以使用敬业签. 敬业签针对待办事项可以设定单次定时提醒.重要事项间隔时间提醒.周期循环提醒和到期延时提醒. Windows电脑 ...

  4. 自定义日历控android,Android自定义日历Calender代码实现

    产品要做签到功能,签到功能要基于一个日历来进行,所以就根据 要求自定义了一个日历 自定义控件相信做android都知道: (1)首先创建一个类,继承一个容器类或者是一个控件 (2)然后就是你需要设置的 ...

  5. 安卓学习笔记--- Android自定义View(CustomCalendar-定制日历控件)

    最近需要做一个日历的控件,感觉使用系统的不能满足自己需求,发现了一个比较不错的自定义日历控件,博主写的很好,转载支持一下. 转载地址: http://blog.csdn.net/xmxkf/artic ...

  6. android仿小米日历,实现一个仿小米日历控件

    先看效果图: 效果图 根据效果,我们可以看到,要实现该控件,需要具备: 容器以及触摸事件处理 周日历布局以及选择,切换上下周处理 月日历布局以及选择,切换上下月处理 首先说说容器 对于其他使用者来说, ...

  7. android 仿旅游日历控件_android 仿预订日历时间选择(如去哪儿,携程

    匿名用户 1级 2018-02-04 回答 看标题就知道了,一个日历选择,类似于去哪儿,携程,酒店预订功能 调用方法 package com.fly.caldroid;import android.a ...

  8. 一个Demo学会用Android兼容包新控件

    2019独角兽企业重金招聘Python工程师标准>>> 前言 伟大的Google为Android推出了一系列的兼容包,最新的就是Design Support Library了,这里我 ...

  9. android自定义波浪图,Android自定义控件--波浪图控件

    今天给大家分享一个android的波浪图控件制作.具体效果如下图所示: 上次有个app使用了这个控件,感觉特别酷炫.今天讲解一下这个控件的思路分析与代码编写. 思路分析: 1.绘制波浪图 2.移动波浪 ...

  10. android自定义刻度线,Android自定义控件之刻度尺控件

    今天我做的是一个自定义刻度尺控件,由于项目需求需要使用刻度尺那样滑动选择,由于对自定义控件的认识还不够深入,于是花了一周多时间才把这个控件给整出来,也是呕心沥血的经历啊,也让我对自定义控件有了自己的认 ...

最新文章

  1. Linux学习笔记-题记
  2. pgp加密软件的简单使用
  3. 天融信防火墙NGFW4000配置
  4. 51单片机 简易光电循迹小车
  5. aes256加密java_使用Java和JCEKS进行AES-256加密
  6. python中str和input_python中eval()函数和input()函数用法解析
  7. 华为java 优招面试题_2017华为优招笔试题
  8. 总结一下数据库的 一对多、多对一、一对一、多对多 关系
  9. android 控件颜色随焦点变化实例
  10. vbs整人代码蓝屏_vbs整人代码
  11. SI4463模块使用心得(无线协议)
  12. 黑苹果声卡、电池驱动
  13. VScode嵌入式开发之入门教程
  14. 2018年南京公积金贷款新政答疑来了!首套房最高可贷50万元/人(附首套房认定标准)...
  15. Simulink仿真入门到精通(十七) Simulink代码生成技术详解
  16. Gitea 的简单介绍
  17. SmartRF04EB修复与修改ID号
  18. 程序员的工资这么高,为什么还会有人离职?
  19. UCS-2 编码范围
  20. GPS定位系统普遍存在的问题

热门文章

  1. AI程序员的远方是诗和梦想的美好?还是骨感无望的现实?
  2. 路透社:谷歌已停止与华为部分合作;联想否认断供华为PC;微软计划直供Linux内核;谷歌无人机快递Wing进军芬兰……...
  3. @所有人,云计算喊你一起来学习!
  4. sass封装h5适配文件
  5. 装完系统还要装什么_家里装了空调还要装空气净化系统吗?会不会太浪费了?...
  6. jq动态渲染后获取不到元素高度_浏览器的渲染机制
  7. 小型微型计算机系统退回修改,小型微型计算机系统
  8. oracle+mybatis查询遇到CHAR类型字段
  9. koa-generator 快速生成 koa2 服务的脚手架工具
  10. 1273 - Unknown collation: 'utf8mb4_0900_ai_ci'