前几天闲来无事,变想做一些小工具玩玩。花了一天多的时间,弄出一个简单日历的View。分为月份模式和星期模式。滚动查看,先上图看看:
上面的是显示的是月份的模式。下面是星期的模式:
CalendarView是一个自定义View,然后通过Viewpager的OnpageChangeListener进行刷新View的数据。Viewpager通过轮回使用View。我默认设置是5个。可以左右无限切换。后面因为我在ViewPager下面加了一个slidingDrawer。打开抽屉就可以直接切换成week的模式。为了使Adapter和OnPageChangeListener两个类可以和CalendarView减少耦合,我增加了一个CalendarViewBuilder类。
CalendarView:
package com.example.calendar.widget;
import com.example.caledar.util.DateUtil;
import com.example.calendar.doim.CustomDate;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
public class CalendarView extends View {
private static final String TAG = "CalendarView";
/**
* 两种模式 (月份和星期)
*/
public static final int MONTH_STYLE = 0;
public static final int WEEK_STYLE = 1;
private static final int TOTAL_COL = 7;
private static final int TOTAL_ROW = 6;
private Paint mCirclePaint;
private Paint mTextPaint;
private int mViewWidth;
private int mViewHight;
private int mCellSpace;
private Row rows[] = new Row[TOTAL_ROW];
private static CustomDate mShowDate;//自定义的日期 包括year month day
public static int style = MONTH_STYLE;
private static final int WEEK = 7;
private CallBack mCallBack;//回调
private int touchSlop;
private boolean callBackCellSpace;
public interface CallBack {
void clickDate(CustomDate date);//回调点击的日期
void onMesureCellHeight(int cellSpace);//回调cell的高度确定slidingDrawer高度
void changeDate(CustomDate date);//回调滑动viewPager改变的日期
}
public CalendarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public CalendarView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CalendarView(Context context) {
super(context);
init(context);
}
public CalendarView(Context context, int style, CallBack mCallBack) {
super(context);
CalendarView.style = style;
this.mCallBack = mCallBack;
init(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < TOTAL_ROW; i++) {
if (rows[i] != null)
rows[i].drawCells(canvas);
}
}
private void init(Context context) {
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setColor(Color.parseColor("#F24949"));
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
initDate();
}
private void initDate() {
if (style == MONTH_STYLE) {
mShowDate = new CustomDate();
} else if(style == WEEK_STYLE ) {
mShowDate = DateUtil.getNextSunday();
}
fillDate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHight = h;
mCellSpace = Math.min(mViewHight / TOTAL_ROW, mViewWidth / TOTAL_COL);
if (!callBackCellSpace) {
mCallBack.onMesureCellHeight(mCellSpace);
callBackCellSpace = true;
}
mTextPaint.setTextSize(mCellSpace / 3);
}
private Cell mClickCell;
private float mDownX;
private float mDownY;
/*
*
* 触摸事件为了确定点击的位置日期
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_UP:
float disX = event.getX() - mDownX;
float disY = event.getY() - mDownY;
if (Math.abs(disX) < touchSlop && Math.abs(disY) < touchSlop) {
int col = (int) (mDownX / mCellSpace);
int row = (int) (mDownY / mCellSpace);
measureClickCell(col, row);
}
break;
}
return true;
}
private void measureClickCell(int col, int row) {
if (col >= TOTAL_COL || row >= TOTAL_ROW)
return;
if (mClickCell != null) {
rows[mClickCell.j].cells[mClickCell.i] = mClickCell;
}
if (rows[row] != null) {
mClickCell = new Cell(rows[row].cells[col].date,
rows[row].cells[col].state, rows[row].cells[col].i,
rows[row].cells[col].j);
rows[row].cells[col].state = State.CLICK_DAY;
CustomDate date = rows[row].cells[col].date;
date.week = col;
mCallBack.clickDate(date);
invalidate();
}
}
// 组
class Row {
public int j;
Row(int j) {
this.j = j;
}
public Cell[] cells = new Cell[TOTAL_COL];
public void drawCells(Canvas canvas) {
for (int i = 0; i < cells.length; i++) {
if (cells[i] != null)
cells[i].drawSelf(canvas);
}
}
}
// 单元格
class Cell {
public CustomDate date;
public State state;
public int i;
public int j;
public Cell(CustomDate date, State state, int i, int j) {
super();
this.date = date;
this.state = state;
this.i = i;
this.j = j;
}
// 绘制一个单元格 如果颜色需要自定义可以修改
public void drawSelf(Canvas canvas) {
switch (state) {
case CURRENT_MONTH_DAY:
mTextPaint.setColor(Color.parseColor("#80000000"));
break;
case NEXT_MONTH_DAY:
case PAST_MONTH_DAY:
mTextPaint.setColor(Color.parseColor("#40000000"));
break;
case TODAY:
mTextPaint.setColor(Color.parseColor("#F24949"));
break;
case CLICK_DAY:
mTextPaint.setColor(Color.parseColor("#fffffe"));
canvas.drawCircle((float) (mCellSpace * (i + 0.5)),
(float) ((j + 0.5) * mCellSpace), mCellSpace / 2,
mCirclePaint);
break;
}
// 绘制文字
String content = date.day+"";
canvas.drawText(content,
(float) ((i+0.5) * mCellSpace - mTextPaint.measureText(content)/2),
(float) ((j + 0.7) * mCellSpace - mTextPaint.measureText(
content, 0, 1) / 2), mTextPaint);
}
}
/**
*
* @author huang
* cell的state
*当前月日期,过去的月的日期,下个月的日期,今天,点击的日期
*
*/
enum State {
CURRENT_MONTH_DAY, PAST_MONTH_DAY, NEXT_MONTH_DAY, TODAY, CLICK_DAY;
}
/**
* 填充日期的数据
*/
private void fillDate() {
if (style == MONTH_STYLE) {
fillMonthDate();
} else if(style == WEEK_STYLE) {
fillWeekDate();
}
mCallBack.changeDate(mShowDate);
}
/**
* 填充星期模式下的数据 默认通过当前日期得到所在星期天的日期,然后依次填充日期
*/
private void fillWeekDate() {
int lastMonthDays = DateUtil.getMonthDays(mShowDate.year, mShowDate.month-1);
rows[0] = new Row(0);
int day = mShowDate.day;
for (int i = TOTAL_COL -1; i >= 0 ; i--) {
day -= 1;
if (day < 1) {
day = lastMonthDays;
}
CustomDate date = CustomDate.modifiDayForObject(mShowDate, day);
if (DateUtil.isToday(date)) {
mClickCell = new Cell(date, State.TODAY, i, 0);
date.week = i;
mCallBack.clickDate(date);
rows[0].cells[i] = new Cell(date, State.CLICK_DAY, i, 0);
continue;
}
rows[0].cells[i] = new Cell(date, State.CURRENT_MONTH_DAY,i, 0);
}
}
/**
* 填充月份模式下数据 通过getWeekDayFromDate得到一个月第一天是星期几就可以算出所有的日期的位置 然后依次填充
* 这里最好重构一下
*/
private void fillMonthDate() {
int monthDay = DateUtil.getCurrentMonthDay();
int lastMonthDays = DateUtil.getMonthDays(mShowDate.year, mShowDate.month - 1);
int currentMonthDays = DateUtil.getMonthDays(mShowDate.year, mShowDate.month);
int firstDayWeek = DateUtil.getWeekDayFromDate(mShowDate.year, mShowDate.month);
boolean isCurrentMonth = false;
if (DateUtil.isCurrentMonth(mShowDate)) {
isCurrentMonth = true;
}
int day = 0;
for (int j = 0; j < TOTAL_ROW; j++) {
rows[j] = new Row(j);
for (int i = 0; i < TOTAL_COL; i++) {
int postion = i + j * TOTAL_COL;
if (postion >= firstDayWeek
&& postion < firstDayWeek + currentMonthDays) {
day++;
if (isCurrentMonth && day == monthDay) {
CustomDate date = CustomDate.modifiDayForObject(mShowDate, day);
mClickCell = new Cell(date,State.TODAY, i,j);
date.week = i;
mCallBack.clickDate(date);
rows[j].cells[i] = new Cell(date,State.CLICK_DAY, i,j);
continue;
}
rows[j].cells[i] = new Cell(CustomDate.modifiDayForObject(mShowDate, day),
State.CURRENT_MONTH_DAY, i, j);
} else if (postion < firstDayWeek) {
rows[j].cells[i] = new Cell(new CustomDate(mShowDate.year, mShowDate.month-1, lastMonthDays - (firstDayWeek- postion - 1)), State.PAST_MONTH_DAY, i, j);
} else if (postion >= firstDayWeek + currentMonthDays) {
rows[j].cells[i] = new Cell((new CustomDate(mShowDate.year, mShowDate.month+1, postion - firstDayWeek - currentMonthDays + 1)), State.NEXT_MONTH_DAY, i, j);
}
}
}
}
public void update() {
fillDate();
invalidate();
}
public void backToday(){
initDate();
invalidate();
}
//切换style
public void switchStyle(int style) {
CalendarView.style = style;
if (style == MONTH_STYLE) {
update();
} else if (style == WEEK_STYLE) {
int firstDayWeek = DateUtil.getWeekDayFromDate(mShowDate.year,
mShowDate.month);
int day = 1 + WEEK - firstDayWeek;
mShowDate.day = day;
update();
}
}
//向右滑动
public void rightSilde() {
if (style == MONTH_STYLE) {
if (mShowDate.month == 12) {
mShowDate.month = 1;
mShowDate.year += 1;
} else {
mShowDate.month += 1;
}
} else if (style == WEEK_STYLE) {
int currentMonthDays = DateUtil.getMonthDays(mShowDate.year, mShowDate.month);
if (mShowDate.day + WEEK > currentMonthDays) {
if (mShowDate.month == 12) {
mShowDate.month = 1;
mShowDate.year += 1;
} else {
mShowDate.month += 1;
}
mShowDate.day = WEEK - currentMonthDays + mShowDate.day;
}else{
mShowDate.day += WEEK;
}
}
update();
}
//向左滑动
public void leftSilde() {
if (style == MONTH_STYLE) {
if (mShowDate.month == 1) {
mShowDate.month = 12;
mShowDate.year -= 1;
} else {
mShowDate.month -= 1;
}
} else if (style == WEEK_STYLE) {
int lastMonthDays = DateUtil.getMonthDays(mShowDate.year, mShowDate.month);
if (mShowDate.day - WEEK < 1) {
if (mShowDate.month == 1) {
mShowDate.month = 12;
mShowDate.year -= 1;
} else {
mShowDate.month -= 1;
}
mShowDate.day = lastMonthDays - WEEK + mShowDate.day;
}else{
mShowDate.day -= WEEK;
}
Log.i(TAG, "leftSilde"+mShowDate.toString());
}
update();
}
}
CalendarViewBuilder:
package com.example.calendar.doim;
import android.content.Context;
import com.example.calendar.widget.CalendarView;
import com.example.calendar.widget.CalendarView.CallBack;
/**
* CalendarView的辅助类
* @author huang
*
*/
public class CalendarViewBuilder {
private CalendarView[] calendarViews;
/**
* 生产多个CalendarView
* @param context
* @param count
* @param style
* @param callBack
* @return
*/
public CalendarView[] createMassCalendarViews(Context context,int count,int style,CallBack callBack){
calendarViews = new CalendarView[count];
for(int i = 0; i < count;i++){
calendarViews[i] = new CalendarView(context, style,callBack);
}
return calendarViews;
}
public CalendarView[] createMassCalendarViews(Context context,int count,CallBack callBack){
return createMassCalendarViews(context, count, CalendarView.MONTH_STYLE,callBack);
}
/**
* 切换CandlendarView的样式
* @param style
*/
public void swtichCalendarViewsStyle(int style){
if(calendarViews != null)
for(int i = 0 ;i < calendarViews.length;i++){
calendarViews[i].switchStyle(style);
}
}
/**
* CandlendarView回到当前日期
*/
public void backTodayCalendarViews(){
if(calendarViews != null)
for(int i = 0 ;i < calendarViews.length;i++){
calendarViews[i].backToday();
}
}
}
为了Viewpager可以双向无限滑动,我重写了ViewPagerAdapter。
CustomViewPagerAdapter:
package com.example.calendar.widget;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
public class CustomViewPagerAdapter extends PagerAdapter {
private V[] views;
public CustomViewPagerAdapter(V[] views) {
super();
this.views = views;
}
@Override
public void finishUpdate(View arg0) {
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
}
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public Object instantiateItem(View arg0, int arg1) {
if (((ViewPager) arg0).getChildCount() == views.length) {
((ViewPager) arg0).removeView(views[arg1 % views.length]);
}
((ViewPager) arg0).addView(views[arg1 % views.length], 0);
return views[arg1 % views.length];
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == (arg1);
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void destroyItem(View arg0, int arg1, Object arg2) {
// TODO Auto-generated method stub
}
@Override
public void startUpdate(View arg0) {
}
public V[] getAllItems() {
return views;
}
}
然后为了实现对CalendarView的滑动时的数据更新,我重写了OnPageChangeListener的方法。这个类还是有一定的耦合,但是如果是项目中大量使用ViewPager。可以增加泛型进行复用。
CalendarViewPagerLisenter:
package com.example.calendar.widget;
import android.support.v4.view.ViewPager.OnPageChangeListener;
public class CalendarViewPagerLisenter implements OnPageChangeListener {
private SildeDirection mDirection = SildeDirection.NO_SILDE;
int mCurrIndex = 498;
private CalendarView[] mShowViews;
public CalendarViewPagerLisenter(CustomViewPagerAdapter viewAdapter) {
super();
this.mShowViews = viewAdapter.getAllItems();
}
@Override
public void onPageSelected(int arg0) {
measureDirection(arg0);
updateCalendarView(arg0);
}
private void updateCalendarView(int arg0) {
if(mDirection == SildeDirection.RIGHT){
mShowViews[arg0 % mShowViews.length].rightSilde();
}else if(mDirection == SildeDirection.LEFT){
mShowViews[arg0 % mShowViews.length].leftSilde();
}
mDirection = SildeDirection.NO_SILDE;
}
/**
* 判断滑动方向
* @param arg0
*/
private void measureDirection(int arg0) {
if (arg0 > mCurrIndex) {
mDirection = SildeDirection.RIGHT;
} else if (arg0 < mCurrIndex) {
mDirection = SildeDirection.LEFT;
}
mCurrIndex = arg0;
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageScrollStateChanged(int arg0) {
}
enum SildeDirection {
RIGHT, LEFT, NO_SILDE;
}
}
这是个简单的demo,但是我把他封装起来,直接调用就可以了。如果有需要的同学,可以直接下载就可以了。已经修改版:支持日期点击事件。最后这个页面功能没有实现。
下载地址:http://download.csdn.net/detail/huangyanbin123/7723323
在这个小demo中我遇到一些问题和思考:
1.如何尽量的减少类之间耦合和增加类内聚。
2.如何使代码易读,好多方法extract Method出来比较烦,涉及参数太多。
3.检查错误。因为少了一个break的原因,我debug了半个小时。(一开始就debug到了,不相信咋会调用这个方法)。
4.如何平衡为了减少new对象使用一些int,还是为了以后增加功能增加一些对象的产生。
5.感觉这只是个demo的命名就感觉很随意,没有约束。
后言:转眼过去了快两年,之前写了个一直有小bug,后面我把它修复放在github上,有需要的同学,可以下载下来改改用。https://github.com/huangyanbin/CalendarView,欢迎点赞。