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

前言:本篇是接前兩篇文章來實現日曆的嵌套滑動。日曆的嵌套滑動是分爲兩個部分來實現的,一部分是用戶滑動RecyclerView時處理RecyclerView的內容和自身的滑動,另一部分是處理Dayview跟隨RecyclerView的滑動。這樣分開處理,有利於減少干擾,簡化邏輯,而本文將實現實現第二部分,即用戶手指在DayView上滑動時的處理。

實現原理

1,當用戶未選中日曆時,手指滑動的距離交給RecyclerView來處理,RecyclerView的top滑動多少距離,則DayView滑動多少距離(同步移動)。
2,當用戶選中某一天時,DayView的最大滑動距離爲選中行數(第一行爲0)乘行高selectRow*child.getCeilHeight(),RecyclerView的滑動自然會覆蓋DayView的底部。因此重點是計算DayView的滑動距離。

DayView跟隨RecyclerView滑動

在Behavior中跟隨滑動很簡單,只需要實現layoutDependsOn和onDependentViewChanged方法即可。重點關注calucte方法,計算DayView應該滑動的距離。

  @Override
public boolean layoutDependsOn(CoordinatorLayout parent, DayView child, View dependency) {
    return dependency instanceof RecyclerView; //讓DayView跟隨RecyclerView滑動
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, DayView child, View dependency) {
    child.setTranslationY(-calucte(child, dependency));//讓DayView跟隨RecyclerView滑動
    return true;
}

/**
 * @param child
 * @param dependency
 * @return 返回DayView應該移動到目標位置的Y偏移值
 */
private int calucte( DayView child,View dependency){
    int dy=0;
    int recview_tranlasteY=(child.getMeasuredHeight()-dependency.getTop()); //recview移動的距離
    int selectRow=child.getSelectedRow();
    if (selectRow !=-1 ){//當用戶點擊了選中時
        dy=recview_tranlasteY;
        int maxY=selectRow*child.getCeilHeight();//DayView可移動的最大距離
        if (recview_tranlasteY > maxY){
            dy=maxY;
        }
    } else {//當用戶未點擊選中時 DayView與recview移動的距離一樣
        dy=recview_tranlasteY;
    }
    return dy;
}

滑動事件的攔截

用戶手指在DayView滑動時該如何處理?上面只是讓DayView跟隨RecyclerView滑動,既用戶滑動RecyclerView時DayView能夠跟隨滑動,但用戶手指在DayView上滑動時,還無法滑動。這兒就需要攔截滑動事件,轉交給RecyclerView來處理,從上面的代碼可知,RecyclerView滑動那麼DayView自然就能滑動。

int mInterceptDownX,mInterceptDownY;
boolean isVerticalScroll;
public boolean onInterceptTouchEvent(CoordinatorLayout parent, DayView child, MotionEvent event) { //是CoordinatorLayout的onInterceptTouchEvent事件處理
    boolean intercepted = false;
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {//不攔截DOWN事件以便DayView能夠處理點擊事件
            intercepted = false;// super.onInterceptTouchEvent(parent,child,event);
            mInterceptDownX=(int)event.getX();
            mInterceptDownY=(int)event.getY();
            downY=mInterceptDownY;
            lastTouchY=downY;
            break;
        }
        case MotionEvent.ACTION_MOVE: {//攔截dayview的MOVE事件 外部攔截法,老套路
            RecyclerView recView=(RecyclerView) parent.getChildAt(1);
            if (mInterceptDownY < recView.getTop() && Math.abs(event.getY() - mInterceptDownY) > mTouchSlop ) {//當用戶點擊的範圍是dayview的範圍內,攔擊滑動事件交給onTouchEvent來處理
                intercepted = true;
                isVerticalScroll = true;
            } else {
                intercepted = false;//  super.onInterceptTouchEvent(parent,child,event);
                isVerticalScroll = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            if (isVerticalScroll){//如果dayview的MOVE事件攔截了,那麼UP事件也攔截
                intercepted = true;
            }else {
                intercepted = false;// super.onInterceptTouchEvent(parent,child,event);
            }
            break;
        }
        default:
            break;
    }
    return intercepted;
}

下面是將滑動事件攔截後轉交給RecyclerView來實現滑動。另外當用戶鬆開手指時,回彈的實現。

float lastTouchY;
float downY;
@Override
public boolean onTouchEvent(CoordinatorLayout parent, DayView child, MotionEvent ev) {//是CoordinatorLayout的onTouchEvent事件處理
    boolean consume = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN: //由於onInterceptTouchEvent不攔截DOWN事件 , 那麼onTouchEvent是獲取不到DOWN的事件

            break;
        case MotionEvent.ACTION_MOVE://此處實現用戶手指的滑動
            float currentY=ev.getY();
            float dy=currentY- lastTouchY;
            lastTouchY =currentY;
            int tranlasteY=caluteDeltaY( (RecyclerView) parent.getChildAt(1),(int) -dy ,child.getMeasuredHeight() ,child.getCeilHeight());
            if (!ViewCompat.canScrollVertically(parent.getChildAt(1), -1)) { //判斷RecyclerView是否滑到頂部了
                parent.getChildAt(1).offsetTopAndBottom(tranlasteY); //直接調用RecyclerView來滑動,那麼DayView就會跟隨滑動
                consume=true;
            }
            break;
        case MotionEvent.ACTION_UP: //此處實現用戶鬆開手指的回彈效果
            float deletaY=ev.getY()- downY;
            int offsetMaxY= child.getMeasuredHeight();
            if (parent.getChildAt(1).getTop() > (offsetMaxY)/2 && Math.abs(deletaY)>mTouchSlop){ //向下滑動
                scrollTo(parent,(RecyclerView)parent.getChildAt(1),child.getMeasuredHeight(),300);
            }else if ( parent.getChildAt(1).getTop() < (offsetMaxY)/2 && Math.abs(deletaY) > mTouchSlop){//向上滑動
                scrollTo(parent,(RecyclerView)parent.getChildAt(1),child.getCeilHeight(),300);
            }
            consume=true;
            break;
    }
    return consume;//super.onTouchEvent(parent, child, ev);
}

/**
 * 用於自動滾到目的位置,實現自動回彈功能
 * @param parent
 * @param child
 * @param y
 * @param duration
 */
private  void scrollTo(final CoordinatorLayout parent, final RecyclerView child, final int y, int duration) {

    final Scroller scroller = new Scroller(parent.getContext());
    scroller.startScroll(0, child.getTop(), 0, y-child.getTop(), duration);   //設置scroller的滾動偏移量
    ViewCompat.postOnAnimation(child, new Runnable() {

        @Override
        public void run() {
            //返回值爲boolean,true說明滾動尚未完成,false說明滾動已經完成。
            // 這是一個很重要的方法,通常放在View.computeScroll()中,用來判斷是否滾動是否結束。
            if (scroller.computeScrollOffset()) {
                int delta = scroller.getCurrY() - child.getTop();
                child.offsetTopAndBottom(delta);
                parent.dispatchDependentViewsChanged(child);
                ViewCompat.postOnAnimation(child, this);
            }
        }
    });
}

/**
 * @param child
 * @param dy
 * @param offsetMaxY
 * @param offsetMinY
 * @return 主要計算用戶滑動是否滑出邊界的情況
 */
private int caluteDeltaY(RecyclerView child, int dy ,int offsetMaxY ,int offsetMinY){
    int deletaY=0;
    int offsetY = child.getTop();
    int targetY=child.getTop()-dy;
    if (targetY < offsetMinY){//滑出上邊界了
        deletaY = offsetY-offsetMinY;
    }else if (targetY > offsetMaxY){//滑出下邊界了
        deletaY = offsetY -offsetMaxY;
    }else {
        deletaY=dy;
    }
    return -deletaY;
}

總結

通過以上三篇文章可以看出,一個看似複雜的嵌套滑動日曆,在經過詳細的的功能拆解後,其實現都是相對簡單的,關鍵在於拆解功能的能力,這就需要長期的積累了,路漫漫其修遠兮,吾將上下而求索。

源碼下載

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

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