Android绘制日历

前言

项目中有时候会遇到需要自定义日历,然后再日历上显示当天是否有日程信息。

如图所示,我们需要先绘制出日历,然后再日历上有日程信息的地方标记小圆点,最后当日历上某一天被点击的时候需要提供一个回调方法,处理点击事件。

数据准备

一个月最多有31天,其中最多有4个整周(28天)刚好是当前月,剩下的的3天可能分布在其余两个月,所以我们日历需要绘制6行。

首先求出当前月的第一天是星期几

firstDayInMonth = CalendarUtil.getFirstDayOfMonth(showYear, showMonth) - 1; 
public static int getFirstDayOfMonth(int year, int month) {
    Calendar calendar = Calendar.getInstance();
    calendar.set(year, month, 1);
    return calendar.get(Calendar.DAY_OF_WEEK);
} 

需要注意的是这里的结果是从周日开始计算的从周日开始计算(日:1 一:2 二:3 三:4 四:5 五:6 六:7)

然后根据firstDayInMonth计算出日历上需要显示上个月的日期

List<Day> lastMouthDay = CalendarUtil.getDays(tempY, tempM); 
if (firstDayInMonth == 0) {//星期天
    days.addAll(lastMouthDay.subList(lastMouthDay.size() - 7, lastMouthDay.size()));
} else {
    days.addAll(lastMouthDay.subList(lastMouthDay.size() - firstDayInMonth, lastMouthDay.size()));
} 

先获取上个月的天数,然后获取倒数几个, days用来存储需要绘制的日期。

接着加入当前月的日期(当前月全部装入days)

days.addAll(CalendarUtil.getDays(showYear, showMonth)); 

最后计算出需要下一个月的日期,因为我们绘制6行,所以一共显示42天。当前days已经存储了上一个月和当前月的日期,42-days.size就是需要下一个月的前几天

List<Day> nextMouthDay = CalendarUtil.getDays(nextYear, nextMonth);
days.addAll(nextMouthDay.subList(0, 42 - days.size())); 

到此,日期数据就准备好了。

绘制日历

绘制之前先初始化一些参数,比如每个日期的宽度,高度等信息

//item高度
rowHeight = (int) (getResources().getDisplayMetrics().density*60);
//item宽度
columnWidth = getWidth() / 7; 

然后开始绘制

for (int i = 0; i < days.size(); i++) {
    int day = days.get(i).getDay();
    String dayText = String.valueOf(day);
    column = (i) / 7;
    raw = (i) % 7;
    float textWidth = textPaint.measureText(dayText);
    Rect rect = new Rect();
    textPaint.getTextBounds(dayText, 0, dayText.length(), rect);
    int textHeight = rect.bottom - rect.top;
    float textX = (columnWidth * raw) + (columnWidth - textWidth) / 2;
    float textY = (rowHeight * (column) + titleHeight) + (rowHeight + textHeight) / 2;
    if (days.get(i).isCurrentDay()) {
        //今天
        textPaint.setColor(defaultColor);
        bgPaint.setColor(Color.parseColor("#f7e1df"));
        RectF selectedRect = new RectF(columnWidth * raw, rowHeight * (column) + titleHeight,
                columnWidth * (raw + 1), rowHeight * (column + 1) + titleHeight);
        canvas.drawRoundRect(selectedRect, 10, 10, bgPaint);
    } else if (days.get(i).getMouth() == showMonth) {
        //本月日期
        textPaint.setColor(defaultColor);
    } else {
        //上一个月和下一个月
        textPaint.setColor(Color.GRAY);
    }
    canvas.drawText(dayText, textX, textY, textPaint);
} 

为了区分显示,今天的日期背景用了粉色标记,当前月的日期字体用了黑色,而上一个月和下一个月用了灰色,效果如图。

此时我们还不清楚日历某一对应的是星期几,所以还需要绘制头部信息

绘制头部

头部信息绘制比较简单,直接一行绘制7个文字即可

for (int week = 0; week < 7; week++) {
    textPaint.setColor(defaultColor);
    String weekText = CalendarUtil.getWeekText(week);
    Rect rect = new Rect();
    textPaint.getTextBounds(weekText, 0, weekText.length(), rect);
    int textY = (int) (titleHeight * 0 + titleHeight / 2 - (textPaint.ascent() + textPaint.descent()) / 2);
    float textX = columnWidth * week + (columnWidth - textPaint.measureText(weekText)) / 2;
    canvas.drawText(weekText, textX, textY, textPaint);
} 


处理点击事件

现在我们的日历还是静态的,不能交互,我们需要处理点击事件

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            downY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            float upX = event.getX();
            float upY = event.getY();
            touchIndexX = (int) (upX / columnWidth);
            if (upY >= 0 && upY <= titleHeight) {
                touchIndexY = 0;
            } else {
                touchIndexY = (int) ((upY - titleHeight) / rowHeight) + 1;
            }
            if (touchIndexX >= 0 && touchIndexX <= 7 && touchIndexY >= 1 && touchIndexY <= 6) {
                if (onCalenderItemClickListener != null) {
                    Day touchDay = days.get((touchIndexY - 1) * 7 + touchIndexX);
                    onCalenderItemClickListener.onCalenderItemClick(touchDay);
                }
                invalidate();
            }

            break;
    }
    return true;
} 

重写onTouchEvent方法,在手指抬起的时候计算出按下的位置位于日历上的那一天,然后回调给外部并刷新重新绘制界面,我们需要修改下绘制日期的地方

if (raw == touchIndexX && (column + 1) == touchIndexY) {
    bgPaint.setColor(Color.parseColor("#222222"));
    textPaint.setColor(Color.WHITE);
    RectF selectedRect = new RectF(columnWidth * touchIndexX, rowHeight * (touchIndexY - 1) + titleHeight,
            columnWidth * (touchIndexX + 1), rowHeight * (touchIndexY) + titleHeight);
    canvas.drawRoundRect(selectedRect, 10, 10, bgPaint);
    touchIndexX = -1;
    touchIndexY = -1;
}
//绘制文字之前先加上面逻辑
canvas.drawText(dayText, textX, textY, textPaint); 

此时,日历就可以交互了,点击选中的日期背景黑色高亮


日程信息添加

最后在绘制日期的时候,我们还需要添加日程信息。那些天具有日程信息需要从外部传入。

public void setHaveThingsDay(List<Day> haveThingsDay) {
    needRequestNet = false;
    this.haveThingsDay = haveThingsDay;
    invalidate();
} 

然后再绘制的时候,遍历需要绘制的日历,查看是否在haveThingsDay中

if (haveThingsDay != null) {
    for (int i = 0; i < days.size(); i++) {
        for (int j = 0; j < haveThingsDay.size(); j++) {
            if(days.get(i).getYear() == haveThingsDay.get(j).getYear()
                    && days.get(i).getMouth() == haveThingsDay.get(j).getMouth()
                    && days.get(i).getDay() == haveThingsDay.get(j).getDay()){
                days.get(i).setHaveThings(true);
                days.get(i).setThings(haveThingsDay.get(j).getThings());
                days.get(i).setId(haveThingsDay.get(j).getId());
                break;
            }else {
                days.get(i).setHaveThings(false);
                days.get(i).setThings("");
            }
        }
    }
} 

月份设置

目前月份信息写死的当前月,我们还需要做到切换上一月/下一月,暴露两个方法出去切换

//上一月
public void toLastMouth() {
    if (showMonth == 0) {//当年1月,上一月则为12月,年份减一
        showMonth = 11;
        showYear--;
    } else {
        showMonth--;
    }
    invalidate();
}
//下一月
public void toNextMouth() {
    if (showMonth == 11) {//当年12月,下一月则为1月,年份减加一
        showMonth = 0;
        showYear++;
    } else {
        showMonth++;
    }
    invalidate();
} 

外部调用

List<Day> haveThingsDay = new ArrayList<>();
Day today = new Day(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DATE));
today.setThings("今天不加班");
haveThingsDay.add(today);
haveThingsDay.add(new Day(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DATE) + 1));
calendarView.setHaveThingsDay(haveThingsDay);
calendarView.setOnCalenderItemClickListener(day -> {
    if (TextUtils.isEmpty(day.getThings())) {
        ToastUtil.show(CalendarActivity.this, "暂无消息");
    } else {
        ToastUtil.show(CalendarActivity.this, day.getThings());
    }
}); 


最后使用比较简单,通过回调的日期查看当前是否有日程,或者根据日期信息请求接口获取具体信息。

写在最后

在技术领域内,没有任何一门课程可以让你学完后一劳永逸,再好的课程也只能是“师傅领进门,修行靠个人”。“学无止境”这句话,在任何技术领域,都不只是良好的习惯,更是程序员和工程师们不被时代淘汰、获得更好机会和发展的必要前提。

如果你觉得自己学习效率低,缺乏正确的指导,可以扫码,加入我们资源丰富,学习氛围浓厚的技术圈一起学习交流吧!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值