最近写了一款日历,包含周日历、月日历以及滑动切换视图,先上效果图:
代码已上传到github:https://github.com/yannecer/NCalendar 项目主要用到了自定义View,ViewPager,RecyclerView和NestedScrollingParent。
本篇文章主要说一下月日历数据、月视图绘制以及点击日期的实现。
数据
数据部分,网上能找到比较完整的工具类,主要是根据本月和上月的天数以及本月第一天是周几来计算。
首先计算上月日期: 由本月的第一天是周几和上个月的天数,得出上月的日期的显示
int temp = lastMonthDays - firstDayOfWeek + 1;//上个月的天数减去本月第一天周几再加上1
再计算本月日期:本月内的数据根据该月的天数跑循环。
再计算下月计算上月日期的显示: 下月的天数显示可以看本月最后一天是周几,根据距离一周最后一天的间隔天数,从1开始直接加上就可以了。
这里要分情况了,有的月份跨5个周,有的月份能跨6个周。计算上没有区别,但是显示的时候会有区别,为了简单,统一成6周,共42个元素,一月多余的用下月日期补充。日期计算肯定使用joda-time了,天数、月份、年份计算都非常简单,有一点,这个库每周是周一开始的,周日历要注意一下。
一月的数据:
List<Integer> date = new ArrayList();
int j = 1;
for (int i = 0; i < 42; i++) {
if (i < firstDayOfWeek) { // 前一个月
int temp = lastMonthDays - firstDayOfWeek + 1;
date .add(temp + i);
} else if (i < days + firstDayOfWeek) { // 本月
int temp = i - firstDayOfWeek + 1;
date .add(temp)
} else { // 下一个yue
date .add(j);
j++;
}
}
这里简化了操作,项目中我把每个数据都转化成了joda-time中的DateTime对象,方便后面操作。 数据有了,接着就是绘制这些数据。
MonthView
MonthView继承于View,重写onDraw(canva)方法。 首先在构造方法中根据颜色和字体大小初始化画笔:
mSorlarPaint = getPaint(mSolarTextColor, mSolarTextSize);
.....
private Paint getPaint(int paintColor, float paintSize) {
Paint paint = new Paint();
paint.setColor(paintColor);
paint.setTextSize(paintSize);
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
return paint;
}
接着就是在onDraw(canva)方法中绘制。 我们先考虑一下我们都需要做哪些事情。需要绘制公历、农历、小圆点、选中的圆环包括后面的点击操作,这些元素确定位置都需要一个矩形(Rect),那么就可以先在这个View里面绘制42个矩形。四个点确定一个矩形,可以在纸上画一下大致的图案,大致画个一两行矩形,应该就找到规律了,感觉有点像以前上学时做的找规律的数学题。
6行7列的一个矩形阵
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();//view的宽度
mHeight = getHeight();//view的高度
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 7; j++) {
Rect rect = new Rect(j * mWidth / 7, i * mHeight / 6, j * mWidth / 7 + mWidth / 7, i * mHeight / 6 + mHeight / 6);
canvas.drawRect(rect,mSorlarPaint);
}
}
}
有了这42个矩形,我们做后面的事情就简单了。
绘制文字
绘制文字 canvas.drawText()会发现,可能会出现文字不在矩形的中心,解决办法参看这篇博客,Android Canvas drawText实现中文垂直居中,
Paint.FontMetricsInt fontMetrics = mSorlarPaint.getFontMetricsInt();
int baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
我们需要在绘制的循环里面要判断这些内容:
1、是不是本月的数据(用颜色区分本月和其他月的数据)
2、是不是今天
3、有没有选中的日期
4、显示不显示农历
其中今天和选中的日期用圆环表示,就需要在当天和选中的日期的矩形中绘制圆环。 已今天为例:
//是今天,且是当月的今天才绘制今天的标识
if (Utils.isToday(dateTime) && Utils.isEqualsMonth(dateTime, mInitialDateTime)) {
mSorlarPaint.setColor(mSelectCircleColor);//画笔设置选中的颜色
int radius = Math.min(Math.min(rect.width() / 2, rect.height() / 2), mSelectCircleRadius);//圆环半径取矩形宽、高和设置半径的最小值
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mSorlarPaint);
mSorlarPaint.setColor(Color.WHITE);//当天的文本设置成白色
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
}
完整的onDraw(Canvas canvas)里面的代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();
mHeight = getHeight();
mRectList.clear();
//6行7列
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 7; j++) {
Rect rect = new Rect(j * mWidth / 7, i * mHeight / 6, j * mWidth / 7 + mWidth / 7, i * mHeight / 6 + mHeight / 6);
mRectList.add(rect);
DateTime dateTime = monthDateTimeList.get(i * 7 + j);
Paint.FontMetricsInt fontMetrics = mSorlarPaint.getFontMetricsInt();
int baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
//判断是不是当月,当月和上下月的颜色不同
if (Utils.isEqualsMonth(dateTime, mInitialDateTime)) {
//当天和选中的日期不绘制农历
if (Utils.isToday(dateTime)) {
mSorlarPaint.setColor(mSelectCircleColor);
int radius = Math.min(Math.min(rect.width() / 2, rect.height() / 2), mSelectCircleRadius);
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mSorlarPaint);
mSorlarPaint.setColor(Color.WHITE);
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
//选中日期不为null绘制空心圆
} else if (mSelectDateTime != null && dateTime.toLocalDate().equals(mSelectDateTime.toLocalDate())) {
mSorlarPaint.setColor(mSelectCircleColor);
int radius = Math.min(Math.min(rect.width() / 2, rect.height() / 2), mSelectCircleRadius);
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mSorlarPaint);
mSorlarPaint.setColor(mHollowCircleColor);
canvas.drawCircle(rect.centerX(), rect.centerY(), radius - mHollowCircleStroke, mSorlarPaint);
mSorlarPaint.setColor(mSolarTextColor);
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
} else {
mSorlarPaint.setColor(mSolarTextColor);
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
drawLunar(canvas, rect, mLunarTextColor, i, j);
}
} else {
mSorlarPaint.setColor(mHintColor);
canvas.drawText(dateTime.getDayOfMonth() + "", rect.centerX(), baseline, mSorlarPaint);
drawLunar(canvas, rect, mHintColor, i, j);
}
//绘制提示的小圆点
if (mPointList.contains(dateTime.toLocalDate().toString())) {
mSorlarPaint.setColor(mPointColor);
canvas.drawCircle(rect.centerX(), rect.bottom-mPointSize, mPointSize, mSorlarPaint);
}
}
}
}
......
//绘制农历
private void drawLunar(Canvas canvas, Rect rect, int color, int i, int j) {
if (isShowLunar) {
mLunarPaint.setColor(color);
String lunar = lunarList.get(i * 7 + j);
canvas.drawText(lunar, rect.centerX(), rect.bottom - Utils.dp2px(getContext(), 5), mLunarPaint);
}
}
里面的一些工具类可参见github上的项目:https://github.com/yannecer/NCalendar
点击事件
点击操作使用了GestureDetector,这个类里面已经定义好了单级,双击,长按等操作,只需要我们重写相应的方法就可以,不用我们在去定义一个点击操作了。 重写MonthView的onTouchEvent(MotionEvent event)方法,交给GestureDetector处理
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
触摸事件交给GestureDetector,当发生单击时,循环刚才绘制文本时的矩形,根据用户点击的XY坐标值判断是在哪个矩形内,我们就知道用户点击的是哪个日期了。
private GestureDetector mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
for (int i = 0; i < mRectList.size(); i++) {
Rect rect = mRectList.get(i);
//判断
if (rect.contains((int) e.getX(), (int) e.getY())) {
DateTime selectDateTime = monthDateTimeList.get(i);
//点击的是上个月
if (Utils.isLastMonth(selectDateTime, mInitialDateTime)) {
onClickMonthViewListener.onClickLastMonth(selectDateTime);
} else if (Utils.isNextMonth(selectDateTime, mInitialDateTime)) {
//点击的是下个月
onClickMonthViewListener.onClickNextMonth(selectDateTime);
} else {
//点击的是本月
onClickMonthViewListener.onClickCurrentMonth(selectDateTime);
}
break;
}
}
return true;
}
});
里面写了一些回调,方便在ViewPager中跳转到相应的月份。剩下的操作放到了ViewPager中完成,如果不是本月就跳转再设置选中的日期,如果是本月,就直接设置选中的日期:
@Override
public void onClickCurrentMonth(DateTime dateTime) {
doClickEvent(dateTime, getCurrentItem());
}
@Override
public void onClickLastMonth(DateTime dateTime) {
int currentItem = getCurrentItem() - 1;
doClickEvent(dateTime, currentItem);
}
@Override
public void onClickNextMonth(DateTime dateTime) {
int currentItem = getCurrentItem() + 1;
doClickEvent(dateTime, currentItem);
}
........
//处理点击
private void doClickEvent(DateTime dateTime, int currentItem) {
MonthCalendar.this.setCurrentItem(currentItem);
MonthView monthView = (MonthView) calendarAdapter.getCalendarViews().get(currentItem);
monthView.setSelectDateTime(dateTime);
if (onClickMonthCalendarListener != null) {
onClickMonthCalendarListener.onClickMonthCalendar(dateTime);
}
}
在doClickEvent(DateTime dateTime, int currentItem)方法中,得到当前的MonthView ,设置选中日期monthView.setSelectDateTime(dateTime); 而在setSelectDateTime(DateTime dateTime)中就是赋值和重绘页面:
public void setSelectDateTime(DateTime dateTime) {
this.mSelectDateTime = dateTime;
invalidate();
}
这样在onDraw(Canvas canvas)中mSelectDateTime!=null,就会绘制选中的圆环了。
MonthView没有重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,是因为这个MonthView是放在继承自ViewPager的MonthCalendar中使用的,只需在布局文件中设置MonthCalendar的layout_width和layout_height即可:
<com.necer.ncalendar.calendar.MonthCalendar
android:id="@+id/monthCalendar"
android:layout_width="match_parent"
android:layout_height="240dp"
android:background="@color/white"
app:selectCircleColor= "@android:color/holo_red_light"
app:pointcolor="#00c8aa"
app:pointSize="1dp"
app:solarTextSize= "15sp"/>