SuperCalendar
簡介
- 博主現在工作在一家教育公司,最近公司的產品狗扔過來一個需求,說要做一個可以周月切換的課表,可以展示用戶在某一天的上課安排。接到這個任務之后我研究了很多的日歷控件,并且抽出了一個calenderlibS。先看一下最后的項目中的效果:
月模式.png
周模式.png
- 看到本篇文章的同學估計也是實驗課或者項目需求中需要一個日歷表,當我接到這個需求的時候,當時腦子壓根連想都沒想,這么通用的控件,GitHub上一搜一大堆不是嘛??墒堑鹊秸嬲銎饋淼臅r候,扎心了老鐵,GitHub上的大神居然異常的不給力,都是實現了基本功能,能夠滑動切換月份,找實現了周月切換功能的開源庫很難。終于我費盡千辛萬苦找到一個能夠完美切換的項目時,你周月切換之后的數據亂的一塌糊涂?。。?!
- 算了,自己擼一個!??!
項目鏈接 SuperCalendar
- 如果你感覺到對你有幫助,歡迎star
- 如果你感覺對代碼有疑惑,或者需要修改的地方,歡迎issue
主要特性
- 日歷樣式完全自定義,拓展性強
- 左右滑動切換上下周月,上下滑動切換周月模式
- 抽屜式周月切換效果
- 標記指定日期(marker)
- 跳轉到指定日期
思路
[圖片上傳失敗...(image-bf19df-1513673145313)]
- 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;
}
- 然后看一下CalendarRenderer的代碼,Renderer承擔了Calendar的繪制任務,首先renderer根據種子日期seedDate填充出Calendar包含的Date數據,calendar中持有一個6*7二維數組來存放日期數據。然后在onDraw的時候通過IDayRenderer來完成對日歷的繪制。當點擊日期改變了日期的狀態時,首先改變對應日期的狀態State,然后重繪Calendar。
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;
}
}
}
- 調用Renderer的draw方法時使用dayRenderer.drawDay(canvas , weeks[row].days[col]),dayRenderer是一個接口,在lib中有一個DayView 的抽象類實現該接口。 其中的drawDay方法完成了對該天到calendar的canvas上的繪制
@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);
}
- 使用繼承自ViewPager的MonthPager來存放calendar的view
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;
}//計算選中日期所在的行數
- 使用CalendarViewAdapter為MonthPager填充calendar的實例
@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);
}
}
}
- (2)MonthPagerBehavior 當recyclerView滑動式,MonthPager做相應的變化。
@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為零時表示滑動停止
}
- 使用IDayRender來實現自定義的日歷效果
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();
}
使用方法
XML布局
- 新建XML布局
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>
自定義日歷樣式
- 新建CustomDayView繼承自DayView并重寫refreshContent 和 copy 兩個方法
@Override
public void refreshContent() {
//你的代碼 你可以在這里定義你的顯示規則
super.refreshContent();
}
@Override
public IDayRenderer copy() {
return new CustomDayView(context , layoutResource);
}
- 新建CustomDayView實例,并作為參數構建CalendarViewAdapter
CustomDayView customDayView = new CustomDayView(
context , R.layout.custom_day);
calendarAdapter = new CalendarViewAdapter(
context ,
onSelectDateListener ,
Calendar.MONTH_TYPE ,
customDayView);
初始化View
- 目前來看 相比于Dialog選擇日歷 我的控件更適合于Activity/Fragment在Activity的
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;
}
}
- 大功告成,如果還不清晰,請下載DEMO
Download
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'
}
[圖片上傳失敗...(image-526acb-1513673145313)]
Licence
Copyright 2017 MagicMashRoom, Inc.