一个仿魅族、小米且实现嵌套滚动功能的签到日历 -- RecyclerViewBehavior篇
前言:本篇是接前一篇文章来实现日历的嵌套滑动。日历的嵌套滑动是分为两个部分来实现的,一部分是用户滑动RecyclerView时处理RecyclerView的内容和自身的滑动,另一部分是处理Dayview跟随RecyclerView的滑动。这样分开处理,有利于减少干扰,简化逻辑,而本文将实现第一部分,下一篇将实现第二部分。
滑动原理
由于滑动的父控件使用的是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);
}