赞
踩
看到本篇文章的同学估计也是实验课或者项目需求中需要一个日历表,当我接到这个需求的时候,当时脑子压根连想都没想,这么通用的控件,GitHub上一搜一大堆不是嘛。可是等到真正做起来的时候,扎心了老铁,GitHub上的大神居然异常的不给力,都是实现了基本功能,能够滑动切换月份,找实现了周月切换功能的开源库很难。终于我费尽千辛万苦找到一个能够完美切换的项目时,你周月切换之后的数据乱的一塌糊涂啊!!!
算了,自己撸一个!!!
https://github.com/MagicMashRoom/SuperCalendar
如果你感觉到对你有帮助,欢迎star
如果你感觉对代码有疑惑,或者需要修改的地方,欢迎issue
日历样式完全自定义,拓展性强
左右滑动切换上下周月,上下滑动切换周月模式
抽屉式周月切换效果
标记指定日期(marker)
跳转到指定日期
Calendar的绘制由CalendarRenderer完成,IDayRenderer实现自定义的日期效果,CalendarAttr中存储日历的属性。
首先看一下Calendar的代码,Calendar主要是初始化Renderer和Attr,然后接受View的生命周期
在OnDraw的时候调用Renderer的onDraw方法,在点击事件onTouchEvent触发时,调用Renderer的点击处理逻辑
private void initAttrAndRenderer() { calendarAttr = new CalendarAttr(); calendarAttr.setWeekArrayType(CalendarAttr.WeekArrayType.Monday); calendarAttr.setCalendarType(CalendarAttr.CalendayType.MONTH); renderer = new CalendarRenderer(this , calendarAttr , context); renderer.setOnSelectDateListener(onSelectDateListener); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); renderer.draw(canvas); } private float posX = 0; private float posY = 0; /* * 触摸事件为了确定点击的位置日期 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: posX = event.getX(); posY = event.getY(); break; case MotionEvent.ACTION_UP: float disX = event.getX() - posX; float disY = event.getY() - posY; if (Math.abs(disX) < touchSlop && Math.abs(disY) < touchSlop) { int col = (int) (posX / cellWidth); int row = (int) (posY / cellHeight); onAdapterSelectListener.cancelSelectState(); renderer.onClickDate(col, row); onAdapterSelectListener.updateSelectState(); invalidate(); } break; } return true; }
private void instantiateMonth() { int lastMonthDays = Utils.getMonthDays(seedDate.year, seedDate.month - 1); // 上个月的天数 int currentMonthDays = Utils.getMonthDays(seedDate.year, seedDate.month); // 当前月的天数 int firstDayPosition = Utils.getFirstDayWeekPosition(seedDate.year, seedDate.month , CalendarViewAdapter.weekArrayType); int day = 0; for (int row = 0; row < Const.TOTAL_ROW; row++) { day = fillWeek(lastMonthDays, currentMonthDays, firstDayPosition, day, row); } } private int fillWeek(int lastMonthDays, int currentMonthDays, int firstDayWeek, int day, int row) { for (int col = 0; col < Const.TOTAL_COL; col++) { int position = col + row * Const.TOTAL_COL; // 单元格位置 if (position >= firstDayWeek && position < firstDayWeek + currentMonthDays) { // 本月的 day ++; fillCurrentMonthDate(day, row, col); } else if (position < firstDayWeek) { //last month instantiateLastMonth(lastMonthDays, firstDayWeek, row, col, position); } else if (position >= firstDayWeek + currentMonthDays) {//next month instantiateNextMonth(currentMonthDays, firstDayWeek, row, col, position); } } return day; } public void draw(Canvas canvas) { for (int row = 0; row < Const.TOTAL_ROW; row++) { if (weeks[row] != null) { for (int col = 0; col < Const.TOTAL_COL; col ++) { if (weeks[row].days[col] != null) { dayRenderer.drawDay(canvas , weeks[row].days[col]); } } } } } public void onClickDate(int col, int row) { if (col >= Const.TOTAL_COL || row >= Const.TOTAL_ROW) return; if (weeks[row] != null) { if(attr.getCalendarType() == CalendarAttr.CalendayType.MONTH) { if(weeks[row].days[col].getState() == State.CURRENT_MONTH){ weeks[row].days[col].setState(State.SELECT); selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectDate(selectedDate); seedDate = selectedDate; } else if (weeks[row].days[col].getState() == State.PAST_MONTH){ selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectOtherMonth(-1); onSelectDateListener.onSelectDate(selectedDate); } else if (weeks[row].days[col].getState() == State.NEXT_MONTH){ selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectOtherMonth(1); onSelectDateListener.onSelectDate(selectedDate); } } else { weeks[row].days[col].setState(State.SELECT); selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectDate(selectedDate); seedDate = selectedDate; } } }
@Override
public void drawDay(Canvas canvas , Day day) {
this.day = day;
refreshContent();
int saveId = canvas.save();
canvas.translate(day.getPosCol() * getMeasuredWidth(),
day.getPosRow() * getMeasuredHeight());
draw(canvas);
canvas.restoreToCount(saveId);
}
viewPageChangeListener = new ViewPager.OnPageChangeListener() {} //新建viewPagerChangeListener @Override protected void onSizeChanged(int w, int h, int oldW, int oldH) { cellHeight = h / 6; super.onSizeChanged(w, h, oldW, oldH); }//重写onSizeChanged,获取dayView的高度 public int getTopMovableDistance() { CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) getAdapter(); rowIndex = calendarViewAdapter.getPagers().get(currentPosition % 3).getSelectedRowIndex(); return cellHeight * rowIndex; }//计算周月切换时在到达选中行之前MonthPager收起的距离 public int getRowIndex() { CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) getAdapter(); rowIndex = calendarViewAdapter.getPagers().get(currentPosition % 3).getSelectedRowIndex(); Log.e("ldf","getRowIndex = " + rowIndex); return rowIndex; }//计算选中日期所在的行数
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); this.currentPosition = position; } @Override public Object instantiateItem(ViewGroup container, int position) { if(position < 2){ return null; } Calendar calendar = calendars.get(position % calendars.size()); if(calendarType == CalendarAttr.CalendayType.MONTH) { CalendarDate current = seedDate.modifyMonth(position - MonthPager.CURRENT_DAY_INDEX); current.setDay(1);//每月的种子日期都是1号 calendar.showDate(current); } else { CalendarDate current = seedDate.modifyWeek(position - MonthPager.CURRENT_DAY_INDEX); if(weekArrayType == 1) { calendar.showDate(Utils.getSaturday(current)); } else { calendar.showDate(Utils.getSunday(current)); }//每周的种子日期为这一周的最后一天 calendar.updateWeek(rowCount); } if (container.getChildCount() == calendars.size()) { container.removeView(calendars.get(position % 3)); } if(container.getChildCount() < calendars.size()) { container.addView(calendar, 0); } else { container.addView(calendar, position % 3); } return calendar; }
日历在切换周月时切换日历中填充的数据
在月模式切换成周模式时,将当前页的seedDate拿出来刷新本页数据,并且更新指定行数的周数据,然后得到seedDate下一周的周日作为下一页的seedDate,刷新下一页的数据,并且更新指定行数的周数据。上一页同理
也是说假设我当前选择的是6月12号周日,处于日历的第二行,也是说下一页的seedDate是6月19号,然后刷新6月19号所在周的数据到选定的第二行。
当切换周月时,把三页的数据都会重新刷新一遍,以保证数据的正确性。
public void switchToMonth() { if(calendars != null && calendars.size() > 0 && calendarType != CalendarAttr.CalendayType.MONTH){ calendarType = CalendarAttr.CalendayType.MONTH; MonthPager.CURRENT_DAY_INDEX = currentPosition; Calendar v = calendars.get(currentPosition % 3);//0 seedDate = v.getSeedDate(); Calendar v1 = calendars.get(currentPosition % 3);//0 v1.switchCalendarType(CalendarAttr.CalendayType.MONTH); v1.showDate(seedDate); Calendar v2 = calendars.get((currentPosition - 1) % 3);//2 v2.switchCalendarType(CalendarAttr.CalendayType.MONTH); CalendarDate last = seedDate.modifyMonth(-1); last.setDay(1); v2.showDate(last); Calendar v3 = calendars.get((currentPosition + 1) % 3);//1 v3.switchCalendarType(CalendarAttr.CalendayType.MONTH); CalendarDate next = seedDate.modifyMonth(1); next.setDay(1); v3.showDate(next); } } public void switchToWeek(int rowIndex) { rowCount = rowIndex; if(calendars != null && calendars.size() > 0 && calendarType != CalendarAttr.CalendayType.WEEK){ calendarType = CalendarAttr.CalendayType.WEEK; MonthPager.CURRENT_DAY_INDEX = currentPosition; Calendar v = calendars.get(currentPosition % 3); seedDate = v.getSeedDate(); rowCount = v.getSelectedRowIndex(); Calendar v1 = calendars.get(currentPosition % 3); v1.switchCalendarType(CalendarAttr.CalendayType.WEEK); v1.showDate(seedDate); v1.updateWeek(rowIndex); Calendar v2 = calendars.get((currentPosition - 1) % 3); v2.switchCalendarType(CalendarAttr.CalendayType.WEEK); CalendarDate last = seedDate.modifyWeek(-1); if(weekArrayType == 1) { v2.showDate(Utils.getSaturday(last)); } else { v2.showDate(Utils.getSunday(last)); }//每周的种子日期为这一周的最后一天 v2.updateWeek(rowIndex); Calendar v3 = calendars.get((currentPosition + 1) % 3); v3.switchCalendarType(CalendarAttr.CalendayType.WEEK); CalendarDate next = seedDate.modifyWeek(1); if(weekArrayType == 1) { v3.showDate(Utils.getSaturday(next)); } else { v3.showDate(Utils.getSunday(next)); }//每周的种子日期为这一周的最后一天 v3.updateWeek(rowIndex); } }
使用CoordinateLayout的特性来做周月模式切换
1.RecyclerViewBehavior
@Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View directTargetChild, View target, int nestedScrollAxes) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager) child.getLayoutManager(); if(linearLayoutManager.findFirstCompletelyVisibleItemPosition() > 0) { return false; } boolean isVertical = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; int firstRowVerticalPosition = (child == null || child.getChildCount() == 0) ? 0 : child.getChildAt(0).getTop(); boolean recycleviewTopStatus = firstRowVerticalPosition >= 0; return isVertical && (recycleviewTopStatus || !Utils.isScrollToBottom()) && child == directTargetChild; } @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); if (child.getTop() <= initOffset && child.getTop() >= minOffset) { consumed[1] = Utils.scroll(child, dy, minOffset, initOffset); saveTop(child.getTop()); } } @Override public void onStopNestedScroll(final CoordinatorLayout parent, final RecyclerView child, View target) { Log.e("ldf","onStopNestedScroll"); super.onStopNestedScroll(parent, child, target); if (!Utils.isScrollToBottom()) { if (initOffset - Utils.loadTop() > Utils.getTouchSlop(context)){ scrollTo(parent, child, minOffset, 200); } else { scrollTo(parent, child, initOffset, 80); } } else { if (Utils.loadTop() - minOffset > Utils.getTouchSlop(context)){ scrollTo(parent, child, initOffset, 200); } else { scrollTo(parent, child, minOffset, 80); } } }
@Override public boolean onDependentViewChanged(CoordinatorLayout parent, MonthPager child, View dependency) { Log.e("ldf","onDependentViewChanged"); CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) child.getAdapter(); if (dependentViewTop != -1) { int dy = dependency.getTop() - dependentViewTop; //dependency对其依赖的view(本例依赖的view是RecycleView) int top = child.getTop(); if( dy > touchSlop){ calendarViewAdapter.switchToMonth(); } else if(dy < - touchSlop){ calendarViewAdapter.switchToWeek(child.getRowIndex()); } if (dy > -top){ dy = -top; } if (dy < -top - child.getTopMovableDistance()){ dy = -top - child.getTopMovableDistance(); } child.offsetTopAndBottom(dy); } else { initRecyclerViewTop = dependency.getTop(); } dependentViewTop = dependency.getTop(); top = child.getTop(); if((initRecyclerViewTop - dependentViewTop) >= child.getCellHeight()) { Utils.setScrollToBottom(false); calendarViewAdapter.switchToWeek(child.getRowIndex()); initRecyclerViewTop = dependentViewTop; } if((dependentViewTop - initRecyclerViewTop) >= child.getCellHeight()) { Utils.setScrollToBottom(true); calendarViewAdapter.switchToMonth(); initRecyclerViewTop = dependentViewTop; } return true; // TODO: 16/12/8 dy为负时表示向上滑动,dy为正时表示向下滑动,dy为零时表示滑动停止 }
DayView实现IDayRenderer,我们新建一个CustomDayView继承自DayView,在里面作自定义的显示
public CustomDayView(Context context, int layoutResource) {
super(context, layoutResource);
dateTv = (TextView) findViewById(R.id.date);
marker = (ImageView) findViewById(R.id.maker);
selectedBackground = findViewById(R.id.selected_background);
todayBackground = findViewById(R.id.today_background);
}
@Override
public void refreshContent() {
renderToday(day.getDate());
renderSelect(day.getState());
renderMarker(day.getDate(), day.getState());
super.refreshContent();
}
RecyclerView的layout_behavior为com.ldf.calendar.behavior.RecyclerViewBehavior
<android.support.design.widget.CoordinatorLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1"> <com.ldf.calendar.view.MonthPager android:id="@+id/calendar_view" android:layout_width="match_parent" android:layout_height="300dp" android:background="#fff"> </com.ldf.calendar.view.MonthPager> <android.support.v7.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="com.ldf.calendar.behavior.RecyclerViewBehavior" android:background="#c2c2c2" android:layout_gravity="bottom"/> </android.support.design.widget.CoordinatorLayout>
@Override
public void refreshContent() {
//你的代码 你可以在这里定义你的显示规则
super.refreshContent();
}
@Override
public IDayRenderer copy() {
return new CustomDayView(context , layoutResource);
}
CustomDayView customDayView = new CustomDayView(
context , R.layout.custom_day);
calendarAdapter = new CalendarViewAdapter(
context ,
onSelectDateListener ,
Calendar.MONTH_TYPE ,
customDayView);
onCreate
或者Fragment的onCreateView
你需要实现这两个方法来启动日历并装填进数据@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_syllabus); initCalendarView(); } private void initCalendarView() { initListener(); CustomDayView customDayView = new CustomDayView( context , R.layout.custom_day); calendarAdapter = new CalendarViewAdapter( context , onSelectDateListener , Calendar.MONTH_TYPE , customDayView); initMarkData(); initMonthPager(); }
使用此方法回调日历点击事件
private void initListener() {
onSelectDateListener = new OnSelectDateListener() {
@Override
public void onSelectDate(CalendarDate date) {
//your code
}
@Override
public void onSelectOtherMonth(int offset) {
//偏移量 -1表示上一个月 , 1表示下一个月
monthPager.selectOtherMonth(offset);
}
};
}
使用此方法初始化日历标记数据
private void initMarkData() {
HashMap markData = new HashMap<>();
//1表示红点,0表示灰点
markData.put("2017-8-9" , "1");
markData.put("2017-7-9" , "0");
markData.put("2017-6-9" , "1");
markData.put("2017-6-10" , "0");
calendarAdapter.setMarkData(markData);
}
使用此方法给MonthPager添加上相关监听
monthPager.addOnPageChangeListener(new MonthPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { mCurrentPage = position; currentCalendars = calendarAdapter.getAllItems(); if(currentCalendars.get(position % currentCalendars.size()) instanceof Calendar){ //you code } } @Override public void onPageScrollStateChanged(int state) { } });
重写onWindowFocusChanged方法,使用此方法得知calendar和day的尺寸
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus && !initiated) {
CalendarDate today = new CalendarDate();
calendarAdapter.notifyDataChanged(today);
initiated = true;
}
}
Gradle:
Step 1. Add it in your root build.gradle at the end of repositories:
allprojects { repositories { ... maven { url 'https://www.jitpack.io' } }}
Step 2. Add the dependency
dependencies {compile 'com.github.MagicMashRoom:SuperCalendar:1.6' }
Copyright 2017 MagicMashRoom, Inc.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。