Android实现高定制化日历控件

Android实现高定制化日历控件

本控件是基于GitHub上的一个日历项目,高度定制化的修改版:
所以附上原项目地址:https://github.com/SundeepK/CompactCalendarView

  • 在原有控件基础上添加头部月份显示
  • 增加根据数据日期显示不同样式
  • 增加根据数据日期选择事件
  • 增加点击外部隐藏日历效果
  • 增加点击事件

  • Android实现高定制化日历控件
    • 简介
    • 实现自定义头部
      • 添加头部视图
      • 添加头部点击事件
    • 为日历控件添加数据根据数据改变显示样式
      • 为控件添加数据
      • 根据数据控制日历相关显示
      • 添加点击事件
    • 补充说明
      • 关于控件动画的问题
      • 关于初始隐藏的问题
      • 关于点击控件以外地方让日历控件隐藏的实现
      • 感谢CompactCalendarView的作者

简介

CompactCalendarView实现了日历默认当天以及选择其他日期的显示、滑动事件、点击事件等功能,是一个封装十分完整的开源项目。 —— [ 项目地址 ]

但介于开发需要,还有许多功能没有实现,特此把详细定制化需求在这里描述一下,希望能帮助到你。

文章控件原型使用了 CompactCalendarView ,  并扩展了很多好用的功能。原控件功能使用方法,具体请参考Github.

实现自定义头部

CompactCalendarView :打开项目发现是一个封装完好的view 具体操作都交给了CompactCalendarController类
主要方法如下:
void drawMonth(Canvas canvas, Calendar monthToDrawCalender, int offset)
这个方法第一个参数不必多说,如果不懂请自行了解自定义view中OnDraw方法
第二个参数用来判断具体的日期
第三个参数用来表示偏移量(这个偏移量是指滑动事件中的偏移量)
主要逻辑:
 for (int dayColumn = 0, dayRow = 0; dayColumn <= 6; dayRow++) {
            if (dayRow == 7) {
                dayRow = 0;
                if (dayColumn <= 6) {
                    dayColumn++;
                }
            }
            if (dayColumn == dayColumnNames.length) {
                break;
            }
            float xPosition = widthPerDay * dayColumn + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight;
            float yPosition = dayRow * heightPerDay + paddingHeight + headHeight;
            if (xPosition >= growFactor && (isAnimatingWithExpose || animationStatus == ANIMATE_INDICATORS) || yPosition >= growFactor) {
                continue;
            }
            if (dayRow == 0) {
                if (shouldDrawDaysHeader) {
                    dayPaint.setColor(calenderTextColor);
                    dayPaint.setTypeface(Typeface.DEFAULT_BOLD);
                    dayPaint.setStyle(Paint.Style.FILL);
                    dayPaint.setColor(calenderTextColor);
                    canvas.drawText(dayColumnNames[dayColumn], xPosition, paddingHeight, dayPaint);
                    dayPaint.setTypeface(Typeface.DEFAULT);
                }
            }

上述代码能明显看出,这是画一个7行7列的一个矩阵,而第一行显示星期几

所以想要在空间上添加头部视图就得在这里做文章

添加头部视图

    void drawHead(Canvas canvas, Calendar yearToMonthCalender, int offset) {
        int year = yearToMonthCalender.get(Calendar.YEAR);    //获取年
        int month = yearToMonthCalender.get(Calendar.MONTH) + 1;   //获取月份,0表示1月份
        dayRect.setColor(Color.argb(255, 66, 66, 66));
        dayRect.setStyle(Paint.Style.FILL);
        dayRect.setTextSize(textSize + 12);
        String text = year + "年" + month + "月";
        float textWidth = dayRect.measureText(text);
        //是不是当前这个页面 如果在所有宽度上添加offset (偏移量)这个头部就会跟着滑动事件进行滑动
        if (width * -monthsScrolledSoFar == offset) {
            Rect lastRect = new Rect((int)(widthPerDay * 2 + paddingWidth + paddingLeft  - paddingRight - lastMonthIcon.getWidth())
                    ,textSize/2
                    ,(int)(widthPerDay * 2 + paddingWidth + paddingLeft  - paddingRight)
                    , textSize+paddingHeight/2);


            Rect nextRect = new Rect((int)(widthPerDay * 4 + paddingWidth + paddingLeft  - paddingRight)
                    ,textSize/2
                    ,(int)(widthPerDay * 4 + paddingWidth + paddingLeft  - paddingRight+nextMonthIcon.getWidth())
                    ,textSize+paddingHeight/2);

            canvas.drawText(text, widthPerDay * 7 / 2 - textWidth / 2, paddingHeight, dayRect);
            canvas.drawBitmap(nextMonthIcon, null , nextRect, null);
            canvas.drawBitmap(lastMonthIcon, null, lastRect, null);
        }

    }

这里简单的画了一个头部,一个现实年月的文本 和两个用于切换月份的按钮,把这个方法放在drawMonth方法中,并把头部位置预留出来 具体代码更改如下:

  void drawMonth(Canvas canvas, Calendar monthToDrawCalender, int offset) {
      ...
      drawHead(canvas, monthToDrawCalender, offset);//添加进来我们写好的方法
      ...
      //在画星期的位置在高度上添加headHeight 把我们们的头部位置留出来
      for (int dayColumn = 0, dayRow = 0; dayColumn <= 6; dayRow++) {
            if (dayRow == 7) {
                dayRow = 0;
                if (dayColumn <= 6) {
                    dayColumn++;
                }
            }
            if (dayColumn == dayColumnNames.length) {
                break;
            }
            float xPosition = widthPerDay * dayColumn + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight;
            float yPosition = dayRow * heightPerDay + paddingHeight + headHeight;
            if (xPosition >= growFactor && (isAnimatingWithExpose || animationStatus == ANIMATE_INDICATORS) || yPosition >= growFactor) {

                continue;
            }
            if (dayRow == 0) {

                if (shouldDrawDaysHeader) {
                    dayPaint.setColor(calenderTextColor);
                    dayPaint.setTypeface(Typeface.DEFAULT_BOLD);
                    dayPaint.setStyle(Paint.Style.FILL);
                    dayPaint.setColor(calenderTextColor);
                    canvas.drawText(dayColumnNames[dayColumn], xPosition, paddingHeight + headHeight, dayPaint);
                    dayPaint.setTypeface(Typeface.DEFAULT);
                }
            }

这样我们的头部就会在 视图上显示出来,这样这个控件就相当于整体下移了一个头部的距离,这样所有的点击事件都会错乱 所以在添加我们相应的点击事件时候,顺便把原有的点击事件进行校对。

添加头部点击事件

先自己添加的头部的点击事件写好,代码如下:

     boolean onIconTouch(MotionEvent event){
        int x = Math.round((paddingLeft + event.getX() - paddingWidth - paddingRight) / widthPerDay);
        int y = Math.round((event.getY()));
        if (x ==2
                && y < headHeight+paddingHeight
                && y > 0) {
            scrollPreviousMonth();
            return true;
        } else if (x == 4
                && y < headHeight+paddingHeight
                && y > 0) {
            scrollNextMonth();
            return true;
        }
        return false;
    }

我把左右两个按钮定位在 第三列和第五列的位置上了,如果不符合自己的需求可自行修改。
之后把我们的点击事件添加到原有的点击事件中,并修正点击事件错乱问题。

    void onSingleTapUp(MotionEvent e) {
        // Don't handle single tap when calendar is scrolling and is not stationary
        if (isScrolling()) {
            return;
        }
        //添加在这里 
        if (onIconTouch(e)){
            return;
        }

        int dayColumn = Math.round((paddingLeft + e.getX() - paddingWidth - paddingRight) / widthPerDay);
        //在这里减去我们头部的高度 就可以准确的获取到行数了
        int dayRow = Math.round((e.getY() - paddingHeight - headHeight) / heightPerDay);

到这里头部添加完成。

为日历控件添加数据根据数据改变显示样式:

我们假设有这样一个需求,我们把每天的数据存储到本地,如果那天本地有数据就可以点击并取出相应数据,并且可选日期为黑色,不可选日子为灰色。
这样的需求就要求我们的日历控件和数据做绑定,那么我们就先从数据入手

为控件添加数据:

List<Calendar> list;//定义一个数据
//写一个添加数据的方法
void setDates(List<DateEntry> dates,Context context){
        this.list = new ArrayList<>();

        if (dates.size() == 0 || dates.isEmpty()){
            dates = null ;
        }else {
            for (int i = 0; i < dates.size() ; i++) {
                Calendar c = Calendar.getInstance(timeZone,locale);
                c.setTime(new Date(dates.get(i).getTime()));
                this.list.add(c);
            }
        }
        init(context);
    }

并把这个方法在CompactCalendarView中开放出来

    public void setDates(List<DateEntry> list , Context context){
        compactCalendarController.setDates(list,context);
    }

这样当数据传入进来后 我们就可以进行相关操作了。

根据数据控制日历相关显示

还是要回到绘制的方法中,

//可以看到在这里原控件已经做了这个日子是不是当前这个月的判断,如果我们需要显示上个月与下个月的日期,那么就得在这里更改
                int day = ((dayRow - 1) * 7 + dayColumn + 1) - firstDayOfMonth;
                int defaultCalenderTextColorToUse = calenderTextColor;
                if (currentCalender.get(Calendar.DAY_OF_MONTH) == day && isSameMonthAsCurrentCalendar && !isAnimatingWithExpose) {
                    drawDayCircleIndicator(currentSelectedDayIndicatorStyle, canvas, xPosition, yPosition, currentSelectedDayBackgroundColor);
                    defaultCalenderTextColorToUse = Color.WHITE;
                } else if (isSameYearAsToday && isSameMonthAsToday && todayDayOfMonth == day && !isAnimatingWithExpose) {
                    drawDayCircleIndicator(currentDayIndicatorStyle, canvas, xPosition, yPosition, currentDayBackgroundColor);
                    defaultCalenderTextColorToUse = currentDayTextColor;
                }

这里笔者就只针对当前显示的月份进行操作,所以在这里添加 else if 代码如下:

                } else if (list == null || list.isEmpty()) {
                    //如果没有数据,全部都为灰色
                    defaultCalenderTextColorToUse = Color.argb(255,189,189,189);
                } else {
                    //如果有数据,遍历数据找到当日数据,颜色设为黑色表示可以选中
                    for (int i = 0; i < list.size(); i++) {
                        Calendar c = list.get(i);
                        if (c.get(Calendar.MONTH) == monthToDrawCalender.get(Calendar.MONTH)
                                && c.get(Calendar.DAY_OF_MONTH) == day) {
                            defaultCalenderTextColorToUse = calenderTextColor;
                            break;
                        } else {
                            defaultCalenderTextColorToUse = Color.argb(255,189,189,189);
                        }
                    }
                }

这里只是通过判断改变了字体颜色,但遍历集合的方式去查找相应数据实在有些不理想,但无奈笔者也想不出什么更好的方式去查找数据,在数据有上限的情况下这个方式的可以实现的。

添加点击事件

画完之后,就是能否进行点击事件了,具体又回到了点击事件的方法中:

         //添加点击标记
          boolean canSelect = false;
          //判断方法与日期显示判断方法一致
            if (list == null || list.isEmpty()) {
                canSelect = false;
            } else {
                for (int i = 0; i < list.size(); i++) {
                    Calendar c = list.get(i);
                    if (c.get(Calendar.MONTH) == calendarWithFirstDayOfMonth.get(Calendar.MONTH)
                            && (c.get(Calendar.DAY_OF_MONTH)-1) == dayOfMonth) {
                        canSelect = true;
                        break;
                    }
                }
            }
            //表示能否响应点击事件
            if (canSelect) {
                calendarWithFirstDayOfMonth.add(Calendar.DATE, dayOfMonth);

                currentCalender.setTimeInMillis(calendarWithFirstDayOfMonth.getTimeInMillis());
                performOnDayClickCallback(currentCalender.getTime());
            }

至此,结合数据部分完毕。

补充说明

关于控件动画的问题

原控件提供了两种显示和隐藏的动画 实际上都是修改其父控件的大小,所以这里的隐藏并不是通过改变Visibility的参数进行的。

关于初始隐藏的问题

根据上述情况,所以想让控件初始隐藏只需要把其父控件的高度设置为0即可,如果使用非拉伸的展开方式还需要把父控件的宽度也设置为0 。

关于点击控件以外地方,让日历控件隐藏的实现

由于开发时间关系,这个点击并没有封装进控件里,但实际上点击以外的地方就是在onTouch方法中当前点击的view不是日历控件即可。

    public boolean onTouch(View v, MotionEvent event) {
        if (v instanceof CompactCalendarView) {

        } else {
            if (shouldShow) {
                if (!compactCalendarView.isAnimating()) {
                    compactCalendarView.hideCalendar();
                    shouldShow = false;
                }
            }
        }
        return false;
    }

感谢CompactCalendarView的作者。


你可能感兴趣的:(android)