自定义 Calendar 实现签到功能

2,415 阅读5分钟

这篇文章没有什么可看性,主要是源码注释太多,推荐自己看源码,更容易理解些,在这里主要介绍,其运作流程,贴代码片段。

先贴源码地址:点此进入源码地址。

自定义View要重写三个方法:onMeasureonLayoutonDraw,这三个方法各有个的作用,onMeasure是对组件的宽高进行测量,onLayout是对子控件的位置进行摆放,onDraw是对自定义控件进行绘制,在《仿探探头像编辑解析》这篇文章中,已经对onMeasure,onLayout方法进行了运用,那个源码注释也很多,如果有兴趣的可以去看看,本章是对onDraw方法进行使用,顺带使用Path对象。

好了,先谈谈为什么我要重复造轮子,要做一个有签到功能的日历,由于自己对自定义的组件ondraw方法还没怎么用过,所以重复造轮子咯,是不是理由不是很充分,没关系,开心就好。

先来张效果图
这里写图片描述

这个CalendarView的API

    String clickLeftMonth();    
    String clickRightMonth();   
    Surface getSurface();       
    String getYearAndmonth();   
    boolean isSelectMore();     
    setSelectMore(boolean flag);
    setFlagData(String[] flags);
    setOnItemClickListener(OnItemClickListener); 
    setWritingFlag(String str); 

OK,先来简述下这个组件跑起来的流程,
1.初始化数据。
2.测量组件大小,即调用了OnMeasure方法
3.调用onDraw方法。


步骤是不是很简单呀?OK,通过源码简单的跑一下流程。

初始化数据

    public CalendarView(Context context) {
        super(context);
        
        init();
    }

    public CalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        
        init();
    }

    /**
     * 初始化数据 ,初始化事件对象 ,初始化日期格式类对象 ,Surface布局对象初始化 ,获取屏幕密度比例 ,设置View背景 ,设置触摸事件
     */
    private void init() {
        
        curDate = selectedStartDate = selectedEndDate = today = new Date();
        
        calendar = Calendar.getInstance();
        
        calendar.setTime(curDate);
        
        surface = new Surface(this);
        
        surface.density = getResources().getDisplayMetrics().density;
        
        setOnTouchListener(this);
    }

这一块看出,在组件进行实例化的时候调用了init方法,然后看见了new Surface() 创建了一个Surface对象。ok来看下这个Surface类,其他的应该都知道是什么。(像我注释这么密的看不懂才怪(*\^__^*))。

    public void init() {
        float temp = height / 7f;
        monthHeight = 0;
        weekHeight = (float) ((temp + temp * 0.3f) * 0.5);
        cellHeight = (height - monthHeight - weekHeight) / 6f;
        cellWidth = width / 7f;
        
        borderPaint = new Paint();
        borderPaint.setColor(cellBorderColor);
        borderPaint.setStyle(Paint.Style.STROKE);
        
        borderWidth = (float) (0.5 * density);
        borderWidth = borderWidth < 1 ? 1 : borderWidth;
        borderPaint.setStrokeWidth(borderWidth);

        
        weekPaint = new Paint();
        weekPaint.setColor(textWeekColor);
        weekPaint.setAntiAlias(true);
        float weekTextSize = weekHeight * 0.6f;
        weekPaint.setTextSize(weekTextSize);
        weekPaint.setTypeface(Typeface.DEFAULT_BOLD);

        
        datePaint = new Paint();
        datePaint.setAntiAlias(true);
        float cellTextSize = cellHeight * 0.3f;
        datePaint.setTextSize(cellTextSize);
        datePaint.setTypeface(Typeface.DEFAULT_BOLD);

        
        boxPath = new Path();
        
        boxPath.rLineTo(width, 0);
        
        boxPath.moveTo(0, monthHeight + weekHeight);
        
        boxPath.rLineTo(width, 0);

        
        for (int i = 1; i < 7; i++) {
            
            boxPath.moveTo(i * cellWidth, monthHeight);
            boxPath.rLineTo(0, height - monthHeight);
            
            boxPath.moveTo(0, monthHeight + weekHeight + i * cellHeight);
            boxPath.rLineTo(width, 0);
        }

        
        cellBgPaint = new Paint();
        cellBgPaint.setAntiAlias(true);
        cellBgPaint.setStyle(Paint.Style.FILL);
        cellBgPaint.setColor(cellSelectBgColor);
    }

其实这个类也没做什么,就一个init方法就是初始化各种画笔,然后动态计算各种高度和宽度。这里面的那个for循环里面的boxPath就是通过path对象记录绘制的表格路径。

ok回到CalendarView类,这个组件被实例化了,就开始进行调用onMeasure方法了。这方法里面没啥可说的就是测量这个组件的大小,确定这个组件需要的宽高是多少如果,有疑问可以看看《仿探探头像编辑解析》这一篇文章。

onMeasure和onLayout会被执行两次,然后才执行onDraw方法,看下这个onDraw方法。

首先调用了这个calculateDate方法。这个方法是动态计算日期的。

/**
     * 计算日期,计算出上月,这月下月的日期装入到一个数组里面进行保存
     */
    private void calculateDate() {
        calendar.setTime(curDate);
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        int dayInWeek = calendar.get(Calendar.DAY_OF_WEEK);
        Log.d(TAG, "day in week:" + dayInWeek);
        int monthStart = dayInWeek;
        monthStart -= 1; 
        curStartIndex = monthStart;
        date[monthStart] = 1;
        
        if (monthStart > 0) {
            calendar.set(Calendar.DAY_OF_MONTH, 0);
            int dayInmonth = calendar.get(Calendar.DAY_OF_MONTH);
            for (int i = monthStart - 1; i >= 0; i--) {
                date[i] = dayInmonth;
                dayInmonth--;
            }
            calendar.set(Calendar.DAY_OF_MONTH, date[0]);
        }
        showFirstDate = calendar.getTime();

        
        calendar.setTime(curDate);
        calendar.add(Calendar.MONTH, 1);
        calendar.set(Calendar.DAY_OF_MONTH, 0);
        int monthDay = calendar.get(Calendar.DAY_OF_MONTH);
        for (int i = 1; i < monthDay; i++) {
            date[monthStart + i] = i + 1;
        }
        curEndIndex = monthStart + monthDay;

        
        for (int i = monthStart + monthDay; i < 42; i++) {
            date[i] = i - (monthStart + monthDay) + 1;
        }
        if (curEndIndex < 42) {
            
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        calendar.set(Calendar.DAY_OF_MONTH, date[41]);
        showLastDate = calendar.getTime();
    }

这个方法动态计算日期,显示计算上个月所剩下的日期装入数组date里面,然后装当前月份的,最后装下个月开头部分日期。

为什么会在这个onDraw方法里面调用呢,因为如果在构造方法里面执行一次就没法执行了,如果我点击下一个月那数据就不变了,onMeasure和onLayout都执行两遍所以不行。因此只能在onDraw方法绘制一次,计算一下。

往下看,这段代码是绘制星期天的。

        
        canvas.drawPath(surface.boxPath, surface.borderPaint);
        
        float weekTextY = surface.monthHeight + surface.weekHeight * 3 / 4f;
        
        for (int i = 0; i < surface.weekText.length; i++) {
            float weekTextX = i
                    * surface.cellWidth
                    + (surface.cellWidth - surface.weekPaint
                    .measureText(surface.weekText[i])) / 2f;
            canvas.drawText(surface.weekText[i], weekTextX, weekTextY,
                    surface.weekPaint);
        }

动态计算星期1-7的位置然后在所处位置绘制文字。

再下面就是绘制选择格子的背景颜色,默认是当前月的当前号数。

    /**
     * @param canvas
     */
    private void drawDownOrSelectedBg(Canvas canvas) {
        
        if (downDate != null) {
            drawCellBg(canvas, downIndex, surface.cellDownBgColor);
        }
        
        if (!selectedEndDate.before(showFirstDate)
                && !selectedStartDate.after(showLastDate)) {
            int[] section = new int[]{-1, -1};
            calendar.setTime(curDate);
            calendar.add(Calendar.MONTH, -1);
            findSelectedIndex(0, curStartIndex, calendar, section);
            if (section[1] == -1) {
                calendar.setTime(curDate);
                findSelectedIndex(curStartIndex, curEndIndex, calendar, section);
            }
            if (section[1] == -1) {
                calendar.setTime(curDate);
                calendar.add(Calendar.MONTH, 1);
                findSelectedIndex(curEndIndex, 42, calendar, section);
            }
            if (section[0] == -1) {
                section[0] = 0;
            }
            if (section[1] == -1) {
                section[1] = 41;
            }
            for (int i = section[0]; i <= section[1];="" i++)="" {="" drawCellBg(canvas,="" i,="" surface.cellSelectBgColor);="" }="" }<="" code=""/>

后面就是开始绘制日期,即将画出来的表格填充数字。

        for (int i = 0; i < num; i++) {
            
            int color = surface.textInstantColor;
            if (isLastMonth(i)) {
                
                color = surface.textOtherColor;
            } else if (isNextMonth(i)) {
                
                color = surface.textOtherColor;
            } else if (todayIndex != -1) {
                
                int flagLen = flagData == null ? 0 : flagData.length;
                for (int j = 0; j < flagLen; j++) {
                    if ((date[i] + "").equals(flagData[j]))
                        drawCellFlag(canvas, i, surface.textFlagBgColor,
                                surface.textFlagColor);
                }
                
                if (i == todayIndex) {
                    
                    color = surface.textTodayColor;
                }
            }
            drawCellText(canvas, i, date[i] + "", color);
        }

在这值得一提的就是这个添加签到标签的方法drawCellFlag。

    /**
     * 在格子的右上角进行绘制标签
     *
     * @param canvas     画布
     * @param index     下标
     * @param bgcolor   背景颜色
     * @param textcolor 字体颜色
     */
    private void drawCellFlag(Canvas canvas, int index, int bgcolor,
                              int textcolor) {
        int x = getXByIndex(index);
        int y = getYByIndex(index);
        
        float left = surface.cellWidth * (x - 1) + surface.borderWidth;
        float top = surface.monthHeight + surface.weekHeight + (y - 1)
                * surface.cellHeight - surface.borderWidth;
        float right = left + surface.cellWidth + surface.borderWidth;
        float botton = top + surface.cellHeight - surface.borderWidth;

        surface.cellBgPaint.setColor(bgcolor);
        
        Path path = new Path();
        path.moveTo(right - surface.cellWidth * 2 / 3, top);
        path.lineTo(right - surface.cellWidth / 4, top);
        path.lineTo(right, botton - surface.cellHeight * 3 / 4);
        path.lineTo(right, botton - surface.cellHeight / 3);
        canvas.drawPath(path, surface.cellBgPaint);

        
        canvas.save();
        
        canvas.rotate((float) 45, right - surface.cellWidth * 3 / 7, botton
                - surface.cellHeight * 5 / 6);
        surface.cellBgPaint.setColor(textcolor);
        
        float a = surface.cellWidth / 4;
        float b = surface.cellHeight / 4;
        float c = (float) Math.sqrt(a * a + b * b);
        surface.cellBgPaint.setTextSize(c * 3 / 5);
        surface.cellBgPaint.setTypeface(Typeface.DEFAULT_BOLD);
        
        canvas.drawText(writingFlag, right - surface.cellWidth * 3 / 7, botton
                - surface.cellHeight * 5 / 6, surface.cellBgPaint);
        
        canvas.restore();
    }

这个方法里面能计算出每个表格的left,right,top,botton的位置,即就可以动态计算梯形四个点,这四个点就是

A(right - surface.cellWidth * 2 / 3, top)
B(right - surface.cellWidth / 4, top)
C(right, botton - surface.cellHeight * 3 / 4)
D(right, botton - surface.cellHeight / 3)

通过Path对象记录这四个点串起来的路径然后canvas绘制就ok了。

而这个标签“签到”的位置也是这样给算出来的。

ok,大概流程讲完了。详细的可以去看源码,里面注释多多,你一定能看懂的。(*^__^*)。

点此进入源码地址。