前言



项目中签到的日历尝试过用GridView或者Recyclerview来实现,用ViewFilper或者ViewPager实现切换动画,功能是实现了,但是第一次启动时,因为GridView或者Recyclerview要创建多个布局,导致界面卡顿,后来想到可以用自定义View的方式来实现,就是可能略微麻烦一些,不过还是尽量实现了一下,于是就有了下面的效果及这篇博客,算是对Calendar的用法总结。

android 开源签到日历显示 手机日历签到_签到


实现过程

日历展示部分用自定义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大同小异,只不过没有日期的选择。先看看效果:

android 开源签到日历显示 手机日历签到_农历日历_02


具体使用方法看代码吧,很简单。




使用



为了方便使用,这里定义了一些属性,如下:

<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