嵌套滑动 滑动冲突
基本原理
在子控件接收到滑动一段距离的请求时, 先询问父控件是否要滑动, 如果滑动了父控件就通知子控件它消耗了一部分滑动距离, 子控件就只处理剩下的滑动距离, 然后子控件滑动完毕后再把剩余的滑动距离传给父控件
如何实现
可参考NestedScrollView,因为它既可以作为嵌套滑动的父控件,也可以作为嵌套滑动的子控件
而RecyclerView只实现了作为子控件的功能,不能作为父控件;所以遇到两个纵向的RecyclerView重叠在一起且对滑动有要求,那么需要对外面那个RecyclerView实现嵌套滑动的父控件功能
1、外列表实现NestedScrollingParent2接口(这是为了兼容低版本,高版本SDK21直接继承于ViewGroup即可,ViewGroup里已经有对应的方法了,直接重写即可)
- onStartNestedScroll : 对应startNestedScroll, 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.
- onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调, 可以让外控件针对嵌套滑动做一些前期工作.
- onNestedPreScroll : 关键方法, 接收内控件处理滑动前的滑动距离信息, 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.
- onNestedScroll : 关键方法, 接收内控件处理完滑动后的滑动距离信息, 在这里外控件可以选择是否处理剩余的滑动距离.
- onStopNestedScroll : 对应stopNestedScroll, 用来做一些收尾工作.
- getNestedScrollAxes : 返回嵌套滑动的方向, 区分横向滑动和竖向滑动, 作用不大
- onNestedPreFling和onNestedFling : 同上略
- PS:可以使用辅助类NestedScrollingParentHelper
2、内列表实现NestedScrollingChild2接口(高版本SDK21直接继承于View即可)
- startNestedScroll : 起始方法, 主要作用是找到接收滑动距离信息的外控件.
- dispatchNestedPreScroll : 在内控件处理滑动前把滑动信息分发给外控件.
- dispatchNestedScroll : 在内控件处理完滑动后把剩下的滑动距离信息分发给外控件.
- stopNestedScroll : 结束方法, 主要作用就是清空嵌套滑动的相关状态
- setNestedScrollingEnabled和isNestedScrollingEnabled : 一对get&set方法, 用来判断控件是否支持嵌套滑动.
- dispatchNestedPreFling和dispatchNestedFling : 跟Scroll的对应方法作用类似, 不过分发的不是滑动信息而是Fling信息
- PS:最好使用辅助类NestedScrollingChildHelper
例子
这里是要实现一个外列表与一个子列表,纵向嵌套在一起的。当往下面滑动时,如果外列表还没到达底部,则滑动的是外列表。当到达底部后,滑动的是子列表;
由于RecyclerView已经实现了作为子控件的嵌套滑动功能,所以不用开发,下面是父控件的代码:
public class SearchHintRecyclerView extends RecyclerView implements NestedScrollingParent { private OverScroller mScroller; public SearchHintRecyclerView(Context context) { this(context, null); } public SearchHintRecyclerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SearchHintRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mScroller = new OverScroller(context); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; //竖向滑动才会开启嵌套滑动的功能 } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { checkScrollFinish(); if (target == null) { return; } if(dy > 0){ //手指向上移动 if(canScrollVertically(1)){ //如果自身的底部还能滑动(还没到达底部) scrollBy(0, dy); //则自身滚动 consumed[1] = dy; //且告诉子控件自身消耗了多少位移 } }else{ //手指向下移动 if(!ViewCompat.canScrollVertically(target, -1)){ //如果子控件的顶部不能滑动(已经到达顶部) scrollBy(0, dy); //则自身滚动 consumed[1] = dy; //且告诉子控件自身消耗了多少位移 } } } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; //不消耗掉子列表的fling事件 } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if(velocityY > 0 || !ViewCompat.canScrollVertically(target, -1)){ //向下滚动or向上滚动时子列表已到达顶部 fling((int) (velocityY * 0.5f)); } return true; } private int nowScrollY = 0; public void fling(int velocityY) { nowScrollY = getScrollY(); mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); ViewCompat.postInvalidateOnAnimation(this); } @Override public void computeScroll() { //当scroll执行fling方法时会触发computeScroll方法 if (mScroller.computeScrollOffset()) { scrollBy(0, mScroller.getCurrY()-nowScrollY); nowScrollY = mScroller.getCurrY(); invalidate(); } } private void checkScrollFinish(){ if(mScroller != null && !mScroller.isFinished()){ mScroller.abortAnimation(); } } }
优化
但是,这样的代码跑起来,滑动没问题了,滚动还是会有些小问题,滚动不是很连贯;
因为这里实现的onNestedPreFling和onNestedFling都是在手指抬起时执行的,这种情况下不能处理像子列表滚动完把剩余的滚动交给外列表来处理;(fling就是一个滚动前瞬间的执行方法,并不是持续执行)
所以,为了优化这个问题,实现的接口NestedScrollingParent应该改成NestedScrollingParent2;子列表中的fling操作应该改成持续的scroll操作(可参考RecyclerView中的源码)
外列表的代码:
public class SearchHintRecyclerView extends RecyclerView implements NestedScrollingParent2 { public SearchHintRecyclerView(Context context) { this(context, null); } public SearchHintRecyclerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SearchHintRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; } @Override public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int nestedScrollAxes, int type) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) { } @Override public void onStopNestedScroll(@NonNull View view, int i) { } @Override public void onNestedScroll(@NonNull View view, int i, int i1, int i2, int i3, int i4) { } private boolean mIsInTouch = false; @Override public boolean dispatchTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_DOWN){ mIsInTouch = true; }else if(ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL){ mIsInTouch = false; } return super.dispatchTouchEvent(ev); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { //触摸时直接停止子控件剩余的滚动事件 if(type == ViewCompat.TYPE_NON_TOUCH && mIsInTouch){ if (target instanceof NestedScrollingChild2) { ((NestedScrollingChild2) target).stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } return; } if((dy > 0 && canScrollVertically(1)) //手指向上移动、父列表未到达底部 || (dy < 0 && !ViewCompat.canScrollVertically(target, -1))){ //手指向下移动、子列表已到达顶部 scrollBy(0, dy); consumed[1] = dy; } } }
再优化
上面优化完还是会有一个问题,就是当滚动(PS:滚动,不是滑动)父列表时,若父列表已达顶部,剩余的滚动将无法在子列表中继续,因为这个事件不算是嵌套滚动,只是单纯的父列表滚动,但是会因此造成一个卡顿的效果;
解决思路:
1、首先将可滚动的子列表作为变量传递给父列表
2、重写父列表的ACTION_UP事件处理,不要用系统的滚动去处理,而是自己定义一个Scroller来执行fing方法
3、重写父列表的computeScroll方法,这个方法在onDraw里会不断被调用,scrollTo、scrollBy、invalidate方法都会引起它的调用;
在这个方法里执行下面的判断:
(1)如果父列表还能再滚动,则使用scrollTo或scrollBy继续滑动父列表
(2)如果父列表不能再滚动了,则自身不再进行滑动等更新操作,并将剩余的滚动传递给子列表,执行子列表的fling方法