前言
项目中签到的日历尝试过用GridView或者Recyclerview来实现,用ViewFilper或者ViewPager实现切换动画,功能是实现了,但是第一次启动时,因为GridView或者Recyclerview要创建多个布局,导致界面卡顿,后来想到可以用自定义View的方式来实现,就是可能略微麻烦一些,不过还是尽量实现了一下,于是就有了下面的效果及这篇博客,算是对Calendar的用法总结。
实现过程
日历展示部分用自定义View的方式实现,左右的切换用ViewPager来实现,根据当前ViewPager的位置(Position)来计算当前的年和月,并画出对应的日期,先来看看日历展示部分View的实现过程。日历总共有6行7列的展示界面,那也就是需要画6*7个日期,那么1号的位置就是1号所对应的周几的位置,假如1号是周五,那么1号对应的下标就为5,1号之前的日期为前一个月的日期,当月最大天数以后的日期为后一个月的日期,以下为关键性代码:
...
//当月信息
int year = 1970 + currentPosition / 12;
int month = currentPosition % 12;
calendar.set(year, month, 1);
int firstDay = calendar.get(Calendar.DAY_OF_WEEK) - 1;
int selectMonthMaxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
//上一个月的最大天数
calendar.add(Calendar.MONTH, -1);
int previousMonthMaxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
...
首先根据ViewPager的当前位置计算需要展示的年月,并设置当前日期为展示年月的1号,获取了该月1号的下标和该月的最大天数后,获取上一个月最大的天数,用于展示上月数据。ok,接下来就是画日历了,这个部分很简单,关键代码如下:
for (int i = 1; i <= 42; i++) {
int copyI = i - 1;
int x = (copyI % 7) * itemWidth + itemWidth / 2;
int y = (copyI / 7) * itemHeight + itemHeight / 2 + ...;
if (i <= firstDay) {//前一月数据
...
int day = previousMonthMaxDay - firstDay + i;
canvas.drawText(String.valueOf(day), x, y, paint);
...
} else if (i > selectMonthMaxDay + firstDay) {//后一月数据
...
int day = i - firstDay - selectMonthMaxDay;
canvas.drawText(String.valueOf(day), x, y, paint);
...
} else {//当前月数据
int day = i - firstDay;
...
canvas.drawText(String.valueOf(day), x, y, paint);
...
}
}
签到标志的绘制需要提供个日期和签到是否成功的标志,然后画日期的时候判断一下即可:
HashMap<String, Boolean> signRecords = new HashMap<>();
signRecords.put("2017-07-12", true);
signRecords.put("2017-07-23", true);
//画签到标志
date.set(year, month, day);
String dateStr = format.format(date.getTime());
if (signRecords.containsKey(dateStr)) {
...
if (signRecords.get(dateStr)) {
canvas.drawBitmap(...);
} else {
canvas.drawBitmap(...);
}
}
OK,这样日历的展示部分就完成了。但实际项目中的需求可能是当用户点击选中某个日期的时候,查看当前的签到信息,那么这个点击位置怎么判断呢,其实也很简单,获取到当前点击位置的x,y值,判断所在的位置:
private int getPosition(float x, float y) {
y -= config.weekHeight;
int y1 = (int) (y / itemHeight);
int x1 = (int) (x / itemWidth);
return y1 * 7 + x1;
}
至于农历的实现就是用网上的公历转农历的算法,换算一下即可,但是需要注意的换算的算法比较复杂,如果我们每个日期都用这个算法换算一下的话,肯定时间复杂度不是很理想了,优化如下:
//如果阳历是当在同一年,同一月,day是lastDay的后一天,并且
//阴历lastLunarDay<29的时候,
//此时的阴历直接在前天的基础上加1,否则重新计算
// false为在前一天的基础上已经修改了,直接可以使用lunar实例
if (lastYear == year && lastMonth == month && day - lastDay == 1) {
if (lastLunarDay > 0 && lastLunarDay < 29) {
//上个日期的基础上加1
lunar.lunarDay = lastLunarDay + 1;
...
}
}
...
//否则重新使用农历转换算法计算日期
这样实际的效果就是,启动和切换都更加流畅,哈哈,至此我们的签到日历的日历展示部分就算完成了,至于日历的切换就是根据当前位置计算日历日期,这里需要优化的地方是,ViewPager的切换如果每次都创建一个自定义View的话,很不好,我们可以把ViewPager中销毁的View,在下一次创建时复用,如此将空间复杂度降到最低,关键代码如下:
...
@Override
public Object instantiateItem(ViewGroup container, int position) {
ZWCalendar calendarView = getContent(position);
container.addView(calendarView);
return calendarView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
destroyViews.add((ZWCalendar) object);
container.removeView((View) object);
}
...
private ZWCalendar getContent(int position) {
ZWCalendar calendarView;
if (destroyViews.size() != 0) {
calendarView = destroyViews.valueAt(0);
destroyViews.remove(calendarView);
} else {
calendarView = new ZWCalendar(getContext());
...
viewSet.add(calendarView);
}
...
return calendarView;
}OK啦!
扩展
既然这种形式的签到日历都实现了,那么顺便删减修改下代码实现另外一种形式的签到日历,里面的逻辑和算法和上面的View大同小异,只不过没有日期的选择。先看看效果:
具体使用方法看代码吧,很简单。
使用
为了方便使用,这里定义了一些属性,如下:
<declare-styleable name="ZWCalendarView">
<attr name="weekHeight" />//周几的标题高度
<attr name="weekTextSize" />
<attr name="weekBackgroundColor" />
<attr name="weekTextColor" />
<attr name="calendarTextSize" />//日历的字体大小
<attr name="calendarTextColor" />
<attr name="isShowOtherMonth" format="boolean" />//是否显示上月和下月的日期
<attr name="otherMonthTextColor" format="color" />
<attr name="isShowLunar" />//是否显示农历
<attr name="lunarTextColor" />//农历字体的颜色,大小等
<attr name="lunarTextSize" />
<attr name="todayTextColor" />
<attr name="selectColor" format="color" />//当前选中的圆形颜色
<attr name="selectTextColor" format="color" />//选中的字体颜色
<attr name="signIconSuccessId" format="integer" />//签到成功的标志
<attr name="signIconErrorId" format="integer" />
<attr name="signIconSize" format="dimension" />//签到标志的大小
<attr name="signTextColor" />//签到字体的颜色
<attr name="limitFutureMonth" />//是否显示未来年月的日历
</declare-styleable>
代码中的使用:
calendarView.setSelectListener(new ZWCalendarView.SelectListener() {
@Override
public void change(int year, int month) {
//当前切换的监听
}
@Override
public void select(int year, int month, int day, int week) {
//当前选中的监听
}
});
//代码选中一个日期
calendarView.selectDate(2017, 9, 3);
//显示上个月
calendarView.showPreviousMonth();
//显示下个月
calendarView.showNextMonth();
//返回今天
calendarView.backToday();
另外那个扩展的签到日历的使用和这个稍稍不同,具体去看代码吧。
额,签到日历虽然实现了,但是还没有用到项目中(你问我为什么不用?,你是程序员你应该懂得),虽然经过了测试,但是可能还是会有一些潜在bug,有问题我再改吧。
代码:
https://github.com/HzwSunshine/SignCalendarProgect