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