欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android实现高定制化日历控件

程序员文章站 2024-03-24 11:58:46
...

Android实现高定制化日历控件

本控件是基于GitHub上的一个日历项目,高度定制化的修改版:
所以附上原项目地址:https://github.com/SundeepK/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的作者。