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的作者。


更多相关文章

  1. Android UI 控件 和 对应监听器详细总结
  2. Android JNI 使用的数据结构JNINativeMethod详解 ||建立Android
  3. Android之SQlite数据库
  4. android平台下基于ffmpeg的swscale模块实现对YUV和RGB数据进行转
  5. HelloWorldAndroid几个控件
  6. android自定义控件:可旋转View:可作为ImageView、ImageButton
  7. Android通过加载其他应用的Dex文件破解关键数据

随机推荐

  1. android RSA和Java RSA加密不一致的坑
  2. 禁止android显示状态栏
  3. Android中文API(138) —— RemoteViews
  4. Android(安卓)SQLiteOpenHelper Sqlite数
  5. Android架构组件-Navigation的使用(一)
  6. android配置X86虚拟机
  7. Android兼容android7.0、及Android8.0以
  8. android中widgets的简单实现
  9. Android(安卓)EventBus使用,粘性事件post
  10. Android(安卓)中各种XML文件的作用