可能是第十好的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”

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

<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”

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-EskBumxB-1715886487198)]

[外链图片转存中…(img-tTteSXR7-1715886487201)]

[外链图片转存中…(img-x52q4O1r-1715886487202)]

[外链图片转存中…(img-EOwaWHCg-1715886487203)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值