可能是第十好的Android 开源 日历 Calendar 仿小米

思路


  • 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核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。

以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)

部分资料一览:

  • 330页PDF Android学习核心笔记(内含8大板块)

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

  • Android BAT大厂面试题(有解析)


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
“@+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核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。

以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)

部分资料一览:

  • 330页PDF Android学习核心笔记(内含8大板块)

[外链图片转存中…(img-vcNNSsFX-1715236130184)]

[外链图片转存中…(img-I9QZSeie-1715236130185)]

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

[外链图片转存中…(img-DVW9arMY-1715236130186)]

  • Android BAT大厂面试题(有解析)

[外链图片转存中…(img-2TlRcWgP-1715236130186)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值