Android实现高定制化日历控件
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的作者。
下一篇: python os