一个仿魅族、小米且实现嵌套滚动功能的签到日历 -- RecyclerViewBehavior篇

前言:本篇是接前一篇文章来实现日历的嵌套滑动。日历的嵌套滑动是分为两个部分来实现的,一部分是用户滑动RecyclerView时处理RecyclerView的内容和自身的滑动,另一部分是处理Dayview跟随RecyclerView的滑动。这样分开处理,有利于减少干扰,简化逻辑,而本文将实现第一部分,下一篇将实现第二部分。

一个仿魅族、小米且实现嵌套滚动功能的签到日历 -- RecyclerViewBehavior篇

滑动原理

由于滑动的父控件使用的是CoordinatorLayout,且其实现了NestedScrollingParent接口的,而RecyclerView又实现了NestedScrollingChild接口的,那么要使用嵌套滑动,只需要实现 CoordinatorLayout.Behavior就可以了。布局代码如下:

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1">

    <com.runmedu.month.daliy.view.DayView
        android:id="@+id/dayView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_behavior="com.runmedu.month.daliy.view.DayViewBehavior"
        android:background="#fff"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rcv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#fff"
        app:layout_behavior="com.runmedu.month.daliy.view.RecyclerViewBehavior"
        android:layout_gravity="bottom"
        android:layout_marginBottom="54dp">
        <!--layout_marginBottom 必须是DayView的一行的高度否则RecyclerView显示不全-->
    </android.support.v7.widget.RecyclerView>

</android.support.design.widget.CoordinatorLayout>

由于CoordinatorLayout是类似FrameLayout的布局,自然无法实现如上图的效果,所以借助于Behavior来让RecyclerView的top值等于Dayview的高度,以实现RecyclerView顶部对齐在DayView的底部。另外由于CoordinatorLayout的类FrameLayout的特性,那么RecyclerView是覆盖在DayView的上方,这点很有用处,当RecyclerView向上滑动时必然覆盖DayView的底部中的一部分。而我们只需要计算需要覆盖的DayView高度,就能正确的实现一个嵌套滚动的日历。但是由于有DayView与RecyclerView都需要滚动,所以需要分为两部分来处理滑动。上半部分的日历滑动由DayViewBehavior来控制,下半部分的滑动由RecyclerViewBehavior来实现。

RecyclerViewBehavior实现

位置

确定RecyclerView与DayView的初始位置,即让RecyclerView顶部对齐DayView的底部。

  @Override
public boolean onLayoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) {
    parent.onLayoutChild(child,layoutDirection);
    child.offsetTopAndBottom(getDayView(parent).getMeasuredHeight());//让RecyclerView的顶部在DayView底部的下方,且RecyclerView是覆盖在DayView之上的,便于后面的嵌套滑动处理
    return true;//告诉CoordinatorLayout改变了布局的初始位置
}

RecyclerView的滚动

RecyclerView自身的滑动还是RecyclerView的类容的滑动,注意consumed[1]的值为RecyclerView自身的滑动的距离,dy-consumed[1]的值即为类容滑动的距离。caluteDeltaY方法用于计算RecyclerView自身需要滑动的距离,以防止滑出边界的情况发生。

 @Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, int dx, int dy, int[] consumed) {
    isTouch=true;
    int translateY=0;
    int offsetMinY= getDayView(coordinatorLayout).getCeilHeight();
    int offsetMaxY= getDayView(coordinatorLayout).getMeasuredHeight();
    if (dy >0 ) {//向上滑动
        translateY= caluteDeltaY(child,dy,offsetMaxY,offsetMinY);
    }else if (dy < 0 && !ViewCompat.canScrollVertically(target, -1)){ //向下滑动
        //此处target与child都是RecyclerView
        //!ViewCompat.canScrollVertically(target, -1)是判断RecyclerView的内容是否滑到顶部
        // 注意向下滑动需要RecyclerView内容滑到顶部之后才滑动offsetTopAndBottom
        translateY= caluteDeltaY(child,dy,offsetMaxY,offsetMinY);
    }
    child.offsetTopAndBottom(translateY);
    consumed[1]=-translateY;//这行很重要,没写对这句,调试这儿搞了半天。consumed的值必须是offsetTopAndBottom的值,否则RecyclerView的滑动会不协调
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}

/**
 * @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;
}

回弹效果

当用户停止滑动RecyclerView时,能够自动弹回的处理。

@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target) {
    int offsetMinY= getDayView(coordinatorLayout).getCeilHeight();
    int offsetMaxY= getDayView(coordinatorLayout).getMeasuredHeight();
    isTouch=false;//用于解决 RecyclerView获取事件后 ACTION_UP事件收不到的bug
    int top=child.getTop();
    if (top> offsetMinY && top < (offsetMaxY)/2) {//向上滑动距离是否大与可滑动距离的一半
        scrollTo(coordinatorLayout, child, offsetMinY, 300);
    }
    if (top < offsetMaxY && !ViewCompat.canScrollVertically(target, -1) && top > (offsetMaxY)/2) {
        scrollTo(coordinatorLayout, child, offsetMaxY, 300);
     }
    super.onStopNestedScroll(coordinatorLayout, child, target);
}

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() && !isTouch ) { //如果用户的手指没有滑动才自动滑动
                int delta = scroller.getCurrY() - child.getTop();
                child.offsetTopAndBottom(delta);
                parent.dispatchDependentViewsChanged(child);
                ViewCompat.postOnAnimation(child, this);
            }
        }
    });
}

禁用快速滑动

另外还要处理一个快速滑动的问题,即用户快速滑动RecyclerView时,禁止RecyclerView的类容快速滑动。

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, float velocityX, float velocityY) {
    if (velocityY >0 ) { //向上的快速滑动事件
        if (child.getTop() > getDayView(coordinatorLayout).getCeilHeight()){ //当日历是展开的时候 拦截掉RecyclerView的Fling事件,即不让RecyclerView的内容快速滚动
            return true;
        }
    }else if (velocityY < 0 ){ //向下的快速滑动事件

    }
    return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

源码下载

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