一個仿魅族、小米且實現嵌套滾動功能的簽到日曆 -- 日曆篇

前言:最近項目需要一個類似魅族嵌套滑動的日曆(如下圖),本來準備直接到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的實現。

源碼下載:

https://download.csdn.net/download/hzmming2008/10872977

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章