前言:最近項目需要一個類似魅族嵌套滑動的日曆(如下圖),本來準備直接到github上面拿一個來用,但是發現他們的日曆要麼沒有實現嵌套滑動,要麼實現的相當複雜(相當於自己寫了一個CoordinatorLayout),並且好多嵌套滑動的過程都不完善。於是決定自己來擼一個日曆。(如下圖)。本文將用CoordinatorLayout和Behavior來快速的實現一個可嵌套滑動的日曆。文章爲本人原創,轉載請註明出處。
需求分析
需要實現的功能點:
從上圖可以看到這個日曆有如下功能點:
1,沒有農曆,取而代之的是打卡時間,且點擊後能有紅色的選中.
2,日曆能夠收縮和展開,且選中行不被能收縮。
3,日曆的下面是一個RecyclerView,且日曆能跟隨RecyclerView來上下滑動而收縮和展開,即嵌套滑動.
實現原理分析
對於功能點1實現方案如下,計算每天是星期幾,找到對應的位置畫出來,並在用戶選中後改變顏色,即自定義view。
對於功能點2和3聯合起來看,其實就是一個嵌套滑動。既然是嵌套滑動,自然想到CoordinatorLayout。對於CoordinatorLayout他有兩個子view,一個是自定義的日曆(DayView),另一個是RecyclerView。RecyclerView的頂部對齊DayView的底部,且RecyclerView覆蓋在DayView的上方,當CoordinatorLayout收到滑動事件時,先判斷DayView是否有選中,再計算DayView需要滑動的距離及RecyclerView需要滑動的距離,最後讓RecyclerView滑動的同時滑動需要距離的DayView,從而形成一個視覺上的連同滑動。既然是嵌套滑動,那麼自然要藉助於Behavior來實現,一個Behavior實現用戶滑動RecyclerView時實現RecyclerView的內容滑動和RecyclerView自身的滑動(RecyclerViewBehavior),另一個實現DayView跟隨RecyclerView滑動(DayViewBehavior)。那麼這個日曆的實現就分爲了兩部分,一部分是自定義日曆視圖,二部分是自定義兩個Behavior來實現嵌套滾動。本文將實現第一部分,後面的文章將實現第二部分。
自定義日曆
日曆實現的要點:
1,計算每個月的天數。
2,計算每月的行數(一行七天)。
3,計算每月第一天的位置,從而確定每一天在屏幕的位置座標。
4,根據座標位置畫出文本。
5,重寫onTouchEvent事件處理用戶的點擊事件。
準備工作
初始化畫筆以及初始畫本月的號數:
public DayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext=context;
touchSlop=ViewConfiguration.get(context).getScaledTouchSlop();
initPait();
initData();
}
初始化畫筆
private void initPait(){
mTextPaint=new TextPaint();
mTextPaint.setTextSize(Utils.dpi2px(mContext,14));
mTextPaint.setColor(Color.BLACK);
mTextPaint.setAntiAlias(true);
mTextPaint.setFakeBoldText(true);
mTextSignPaint=new TextPaint();
mTextSignPaint.setTextSize(Utils.dpi2px(mContext,12));
mTextSignPaint.setColor(Color.parseColor("#ff25adff"));
mTextSignPaint.setAntiAlias(true);
mTextSignPaint.setFakeBoldText(false);
mRectPaint=new Paint();
mRectPaint.setStrokeWidth(2);
mRectPaint.setColor(Color.RED);
mRectPaint.setStyle(Paint.Style.STROKE);//設置空心
mRectPaint.setAntiAlias(true);
}
默認加載當前月的日曆數據
private void initData(){
CalendarDate mTempDate = new CalendarDate(Utils.getYear(),Utils.getMonth(),Utils.getDay());
dealData(mTempDate);
}
日曆默認寬高
處理默認寬高的日曆問題。即當用戶設置DayView的寬高爲
android:layout_width=“wrap_content”,android:layout_height=“wrap_content”
時,DayView應該顯示的大小。(沒什麼好說的,老套路,可翻看我以前博客)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //主要處理用戶使用DayView時,寬、高設置爲wrap_content的情況
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){//寬高都設置爲wrap_content
setMeasuredDimension(widthSpecSize,defaultHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){//寬設置爲wrap_content
setMeasuredDimension(widthSpecSize,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){//高設置爲wrap_content
setMeasuredDimension(widthSpecSize, defaultHeight);
}else{//寬高都設置爲match_parenth或具體的dp值
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
畫出日曆
首先計算每天在屏幕的座標,並測量和計算出文本的座標位置,最後畫出每天的號數以及時間。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
ceilWidth = getMeasuredWidth()/7;
ceilHeight= getMeasuredHeight()/ROW;
resetPos();
drawEveryDay(canvas);
}
/**
* 設置每天左上角的座標
*/
private void resetPos(){
if (dayList==null || dayList.isEmpty()) return;
int firstPos = Utils.getFirstDayWeekPosition(dayList.get(0).getYear(),dayList.get(0).getMonth(), CalendarAttr.WeekArrayType.Monday);
for (int i=0; i<dayList.size();i++){
int x = ((firstPos+ dayList.get(i).getDay()-1) % 7)*ceilWidth;
int y = ((int) Math.ceil((dayList.get(i).getDay()+ firstPos)/7.0f)-1) * ceilHeight;;
dayList.get(i).setLeftX(x);
dayList.get(i).setLeftY(y);
}
}
/** 畫出每天的文本
* @param canvas
*/
private void drawEveryDay(Canvas canvas){
if (dayList==null || dayList.isEmpty()) return;
for (int i=0; i<dayList.size();i++){
if (dayList.get(i).isSelected()){
//畫選中的矩形
mTextPaint.setColor(Color.WHITE);
mRectPaint.setStyle(Paint.Style.FILL);
canvas.drawRoundRect(dayList.get(i).getLeftX(), dayList.get(i).getLeftY(),
dayList.get(i).getLeftX() + ceilWidth,
dayList.get(i).getLeftY() + ceilHeight,5,5, mRectPaint);
}else {
mTextPaint.setColor(Color.BLACK);
mRectPaint.setStyle(Paint.Style.STROKE);
}
Rect rectDay = cacluate(dayList.get(i).getDay()+"",mTextPaint);
Rect rectTime = cacluate(dayList.get(i).getFirstTime()+"",mTextSignPaint);
//畫日期
int offX= (ceilWidth-rectDay.width())/2;
int offY= (ceilHeight-rectTime.height()*2-rectDay.height()-divTextHeight*2)/2 + rectDay.height();
canvas.drawText(dayList.get(i).getDay()+"",dayList.get(i).getLeftX()+offX,
dayList.get(i).getLeftY()+offY,mTextPaint);
//畫第一次簽到時間
offX= (ceilWidth-rectTime.width())/2;
offY= offY + divTextHeight + rectTime.height();
canvas.drawText(dayList.get(i).getFirstTime(),dayList.get(i).getLeftX()+offX,
dayList.get(i).getLeftY()+offY,mTextSignPaint);
//畫最後一次簽到時間
offX= (ceilWidth-rectTime.width())/2;
offY= offY + divTextHeight + rectTime.height();
canvas.drawText(dayList.get(i).getLastTime(),dayList.get(i).getLeftX()+offX,
dayList.get(i).getLeftY()+offY,mTextSignPaint);
//今天 畫個空的矩形
if (Utils.getYear()== dayList.get(i).getYear() &&
Utils.getMonth()== dayList.get(i).getMonth() &&
Utils.getDay()== dayList.get(i).getDay()) {//日曆中爲今天
mRectPaint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(dayList.get(i).getLeftX(), dayList.get(i).getLeftY(),
dayList.get(i).getLeftX() + ceilWidth, dayList.get(i).getLeftY() + ceilHeight, 5, 5,mRectPaint);
}
}
}
點擊處理
用戶點擊某天時,根據用戶點擊的座標位置,找到對應的號數設置,並通知view重繪。
/*
* 觸摸事件爲了確定點擊的位置日期
*/
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 / ceilWidth);
int row = (int) (posY / ceilHeight);
findSelected(col,row);//找到選中的行
invalidate();
}
break;
}
return true;
}
/**
* 查找選中的日期
* @param col 列
* @param row 行
*/
private void findSelected(int col ,int row){
if (dayList==null || dayList.isEmpty()) return;
int firstPos = Utils.getFirstDayWeekPosition(dayList.get(0).getYear(),dayList.get(0).getMonth(), CalendarAttr.WeekArrayType.Monday);
for (int i=0; i<dayList.size();i++){
int x = (firstPos+ dayList.get(i).getDay()-1) % 7;
int y = (int) Math.ceil((dayList.get(i).getDay()+ firstPos)/7.0f)-1;;
if (x==col && row ==y){
cancelAllSelected();
dayList.get(i).setSelected(true);
mSelectedRow = row;
}
}
}
/**
* 取消所有選中
*/
private void cancelAllSelected(){
if (dayList==null || dayList.isEmpty()) return;
for (int i=0; i<dayList.size();i++){
dayList.get(i).setSelected(false);
}
}
總結:以上是自定義一個日曆的完整過程,由於非常簡單所以寫得不是很詳細,難點是每天的座標計算,具體可以看源代碼。下一篇將介紹RecyclerViewBehavior的實現。