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

304c8e70d0bd

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

304c8e70d0bd

304c8e70d0bd

304c8e70d0bd

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

一、主要功能

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,但自己动手实现一个还是收获满满,一个看起来简单的东西,只有亲自尝试了才能体会到其中的滋味,最后希望对大家有所帮助吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值