前言
有時候我們會有需要在日曆上選擇一個日期範圍的這種需求,先選一個開始日期,然後再選結束日期,如酒店入住日期和離店日期選擇,美團攜程這種預定酒店的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地址
源碼
後續會考慮把它做成三方庫的形式方便大家使用。