前言
有时候我们会有需要在日历上选择一个日期范围的这种需求,先选一个开始日期,然后再选结束日期,如酒店入住日期和离店日期选择,美团携程这种预定酒店的app都有这种操作。那么这种需求该如何实现呢?先看一下要实现的效果。
实现思路
如上图所见,首先是展示出了一个月份列表,然后进行选择入住日期和离店日期。这种日历效果该如何实现呢, 用系统的组件是实现不了的需要自定义日历组件,写一个自定义view来展示日期,然后用RecyclerView来展示一个日历列表,每个item都是一个自定日历view,这样就可以显示多个月份的日期,然后在监听日期点击事件,实现选择开始日期和结束日期的逻辑即可。这是实现的方式一,应该也是首先会想到的实现方式,也是没有任何问题的可以实现的。但是呢其实还有另外一种实现方式,不需要自定义日历控件,就可以实现这种需求,用RecyclerView+GridLayoutManager就可以实现,今天我们说的就是这种方式实行此需求。
RecyclerView+GridLayoutManager实现日期范围选择的日历效果
我们知道RecyclerView的GridLayoutManager可以实现网格布局的效果,我们就用这个来实现日历的显示,我们可以看到首先日历的头部是有 周几的显示的,从周日 然后是周一二三四五六,一行是显示七天的日期数据。然后我们就可以设置RecylerView 的GridLayoutManager一行显示7个item展示7天的日期,首先每一行的第一个日期肯定要是周日,第二个显示的是周一的日期,也就是说一个月的第一天肯定是周一到周日的某个周几,如果某个月的1号是周日我们就绘制在第一行的第一个,如果1号是周一就绘制在第一行的第二个位置。一次类推就可以绘制出每个月的每一天1号到31号,就绘制出每个月的月日历,而且都能够跟顶部的周几对上。
具体实现
- 首先我们要按照每一行显示7个日期,而且第一个日期是周日后面是周一到周六的形式来生成每个月的日历数据。生成日历数据的逻辑是要判断某个月第一天是周几,如果是周六则在周日到周五来补充空的item来占位即可,如果是周日那正好不用补空,然后我们要判断一个月最后一天是周几如果是周日则需要在后面补充6个空的占位,依次类推,处理完一个月的开始日期结束日期,中间的日期照常生成即可,无需特殊处理,最终将数据存储在数组里即可。
/**
* 生成日历数据
*/
private List<DateBean> days(String sDate, String eDate) {
List<DateBean> dateBeans = new ArrayList<>();
try {
Calendar calendar = Calendar.getInstance();
//日期格式化
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat formatYYYYMM = new SimpleDateFormat("yyyy-MM");
//起始日期
Date startDate = new Date();
calendar.setTime(startDate);
//结束日期
calendar.add(Calendar.MONTH, 5);
Date endDate = new Date(calendar.getTimeInMillis());
Log.d(TAG, "startDate:" + format.format(startDate) + "----------endDate:" + format.format(endDate));
//格式化开始日期和结束日期为 yyyy-mm-dd格式
String endDateStr = format.format(endDate);
endDate = format.parse(endDateStr);
String startDateStr = format.format(startDate);
startDate = format.parse(startDateStr);
calendar.setTime(startDate);
Log.d(TAG, "startDateStr:" + startDateStr + "---------endDate:" + format.format(endDate));
Log.d(TAG, "endDateStr:" + endDateStr + "---------endDate:" + format.format(endDate));
calendar.set(Calendar.DAY_OF_MONTH, 1);
Calendar monthCalendar = Calendar.getInstance();
//按月生成日历 每行7个 最多6行 42个
//每一行有七个日期 日 一 二 三 四 五 六 的顺序
for (; calendar.getTimeInMillis() <= endDate.getTime(); ) {
//月份item
DateBean monthDateBean = new DateBean();
monthDateBean.setDate(calendar.getTime());
monthDateBean.setMonthStr(formatYYYYMM.format(monthDateBean.getDate()));
monthDateBean.setItemType(DateBean.item_type_month);
dateBeans.add(monthDateBean);
//获取一个月结束的日期和开始日期
monthCalendar.setTime(calendar.getTime());
monthCalendar.set(Calendar.DAY_OF_MONTH, 1);
Date startMonthDay = calendar.getTime();
monthCalendar.add(Calendar.MONTH, 1);
monthCalendar.add(Calendar.DAY_OF_MONTH, -1);
Date endMonthDay = monthCalendar.getTime();
//重置为本月开始
monthCalendar.set(Calendar.DAY_OF_MONTH, 1);
Log.d(TAG, "月份的开始日期:" + format.format(startMonthDay) + "---------结束日期:" + format.format(endMonthDay));
for (; monthCalendar.getTimeInMillis() <= endMonthDay.getTime(); ) {
//生成单个月的日历
//处理一个月开始的第一天
if (monthCalendar.get(Calendar.DAY_OF_MONTH) == 1) {
//看某个月第一天是周几
int weekDay = monthCalendar.get(Calendar.DAY_OF_WEEK);
switch (weekDay) {
case 1:
//周日
break;
case 2:
//周一
addDatePlaceholder(dateBeans, 1, monthDateBean.getMonthStr());
break;
case 3:
//周二
addDatePlaceholder(dateBeans, 2, monthDateBean.getMonthStr());
break;
case 4:
//周三
addDatePlaceholder(dateBeans, 3, monthDateBean.getMonthStr());
break;
case 5:
//周四
addDatePlaceholder(dateBeans, 4, monthDateBean.getMonthStr());
break;
case 6:
addDatePlaceholder(dateBeans, 5, monthDateBean.getMonthStr());
//周五
break;
case 7:
addDatePlaceholder(dateBeans, 6, monthDateBean.getMonthStr());
//周六
break;
}
}
//生成某一天日期实体 日item
DateBean dateBean = new DateBean();
dateBean.setDate(monthCalendar.getTime());
dateBean.setDay(monthCalendar.get(Calendar.DAY_OF_MONTH) + "");
dateBean.setMonthStr(monthDateBean.getMonthStr());
dateBeans.add(dateBean);
//处理一个月的最后一天
if (monthCalendar.getTimeInMillis() == endMonthDay.getTime()) {
//看某个月第一天是周几
int weekDay = monthCalendar.get(Calendar.DAY_OF_WEEK);
switch (weekDay) {
case 1:
//周日
addDatePlaceholder(dateBeans, 6, monthDateBean.getMonthStr());
break;
case 2:
//周一
addDatePlaceholder(dateBeans, 5, monthDateBean.getMonthStr());
break;
case 3:
//周二
addDatePlaceholder(dateBeans, 4, monthDateBean.getMonthStr());
break;
case 4:
//周三
addDatePlaceholder(dateBeans, 3, monthDateBean.getMonthStr());
break;
case 5:
//周四
addDatePlaceholder(dateBeans, 2, monthDateBean.getMonthStr());
break;
case 6:
addDatePlaceholder(dateBeans, 1, monthDateBean.getMonthStr());
//周5
break;
}
}
//天数加1
monthCalendar.add(Calendar.DAY_OF_MONTH, 1);
}
Log.d(TAG, "日期" + format.format(calendar.getTime()) + "----周几" + getWeekStr(calendar.get(Calendar.DAY_OF_WEEK) + ""));
//月份加1
calendar.add(Calendar.MONTH, 1);
}
} catch (Exception ex) {
}
return dateBeans;
}
//添加空的日期占位
private void addDatePlaceholder(List<DateBean> dateBeans, int count, String monthStr) {
for (int i = 0; i < count; i++) {
DateBean dateBean = new DateBean();
dateBean.setMonthStr(monthStr);
dateBeans.add(dateBean);
}
}
private String getWeekStr(String mWay) {
if ("1".equals(mWay)) {
mWay = "天";
} else if ("2".equals(mWay)) {
mWay = "一";
} else if ("3".equals(mWay)) {
mWay = "二";
} else if ("4".equals(mWay)) {
mWay = "三";
} else if ("5".equals(mWay)) {
mWay = "四";
} else if ("6".equals(mWay)) {
mWay = "五";
} else if ("7".equals(mWay)) {
mWay = "六";
}
return mWay;
}
这样就可以用RecyclerView来展示出日历数据了,下面是通过GridLayoutManagere来控制一行展示7个Item如果是一个月份头标题(2018-12)Item则独占一行
GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 7);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int i) {
if (DateBean.item_type_month == adapter.data.get(i).getItemType()) {
return 7;
} else {
return 1;
}
}
});
recyclerView.setLayoutManager(gridLayoutManager);
这样就实现了整个日历的展示效果,开始时间和结束时间的选择逻辑,根据相应的交互逻辑实现即可,在此不赘述了,具体见源码。
实现月份标题悬停的效果
我们仔细看效果图可以发现,月份标题是有一个悬停和慢慢推走的效果的。这个效果可以用ItemDecoration装饰来实现。具体实现是继承ItemDecoration 重写OnDrawOver方法在这个方法要做这么几件事
- 绘制出当前月份标题
如何获取当前要绘制的月份标题是几月份呢?我们RecyclerView的adapter中的数据源DataBean每个日期item都存储了他对应的日期标题,这个日期对应的月份,可以通过 RecyclerView的getAdapter()方法获取Adapter然后通过RecyclerView 的getChildAdapterPosition(fistView)来获取某个itemView在adapter对应的位置 然后从Adapter的数据源中获取每个item的对应的月份。 - 如何实现月份标题推走的效果
逻辑是首先找出当前所有可见的Item的第一个月份标题类型的Item这个Item是当我们滑动列表时下一个悬停的月份标题。然后我们获取这个Item距离顶部的距离view.getTop()当它距离顶部的距离小于等于我们月份标题的高度时,假如标题的高度是150,我们绘制顶部的月份标题顶部的位置就是 150-view.getTop()这样随着位置的推移就会有一个慢慢推走的效果。代码如下
public class MyItemD extends RecyclerView.ItemDecoration {
Paint paint=new Paint();
Paint colorPaint=new Paint();
Paint linePaint=new Paint();
public MyItemD(){
paint.setColor(Color.parseColor("#ffffff"));
paint.setStyle(Paint.Style.FILL);
colorPaint.setColor(Color.parseColor("#ff6600"));
colorPaint.setAntiAlias(true);
linePaint.setAntiAlias(true);
linePaint.setColor(Color.parseColor("#dddddd"));
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if(parent.getChildCount()<=0){
return;
}
//头部的高度
int height=50;
final float scale = parent.getContext().getResources().getDisplayMetrics().density;
height= (int) (height*scale+0.5f);
//获取第一个可见的view,通过此view获取其对应的月份
CalendarList.CalendarAdapter a=(CalendarList.CalendarAdapter) parent.getAdapter();
View fistView=parent.getChildAt(0);
String text=a.data.get(parent.getChildAdapterPosition(fistView)).getMonthStr();
String fistMonthStr="";
int fistViewTop=0;
//查找当前可见的itemView中第一个月份类型的item
for(int i=0;i<parent.getChildCount();i++){
View v=parent.getChildAt(i);
if(2==parent.getChildViewHolder(v).getItemViewType()){
fistMonthStr=a.data.get(parent.getChildAdapterPosition(v)).getMonthStr();
fistViewTop=v.getTop();
break;
}
}
//计算偏移量
int topOffset=0;
if(!fistMonthStr.equals(text)&&fistViewTop<height){
//前提是第一个可见的月份item不是当前显示的月份和距离的顶部的距离小于头部的高度
topOffset=height-fistViewTop;
}
int t=0-topOffset;
//绘制头部区域
c.drawRect(parent.getLeft(),t,parent.getRight(),t+height,paint);
colorPaint.setTextAlign(Paint.Align.CENTER);
colorPaint.setTextSize(15*scale+0.5f);
//绘制头部月份文字
c.drawText(text,parent.getRight()/2,(t+height)/2,colorPaint);
}
}
github地址
源码
后续会考虑把它做成三方库的形式方便大家使用。