viewpager2+recyclerview/nestscrollview滑动冲突问题

9,946 阅读4分钟
  • 一般大多数app主页都会有横向带tab,每个tab下面是列表的页面,一般想到的是用viewpager2/viewpager+recyclerview来实现,但是这里一般会有问题,比如在用viewpager2+列表recyclerview实现tab左右滑动切换和列表竖向滑动,在上下滑动列表的recyclerview时候,会带动了横向tab的滑动,这究竟是什么原因导致的呢:

我们都知道viewpager2其实在横向滑动的时候,是通过recyclerview来实现的,内部通过RecyclerViewImpl来实现的recyclerview,而RecyclerViewImpl内部主要是重写了recyclerview的onTouchEvent和onInterceptTouchEvent事件,在重写这两个事件的时候,其实也没做什么工作,相当于调用了super的方法:

@Override
public boolean onTouchEvent(MotionEvent event) {
    return isUserInputEnabled() && super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
}

所以回归到上面说的冲突问题,在内部的竖向列表recyclerview滑动事件和横向的父recyclerview滑动冲突了。所以可以回归到viewgroup到view事件分发的过程了,viewgroup首先在action_down过程中传递到了dispatchTouchEvent方法,在该方法中,询问onInterceptTouchEvent方法要不要拦截子view的touch事件,在拦截之前,会判断mGroupFlags变量是不是等于FLAG_DISALLOW_INTERCEPT常量,如果等于则直接不进行拦截,我们可以在viewgroup中找到mGroupFlags在什么时候被设置了FLAG_DISALLOW_INTERCEPT

在viewgroup的requestDisallowInterceptTouchEvent方法传true,mGroupFlags就会等于FLAG_DISALLOW_INTERCEPT常量

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;	
    }
    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

如果在viewgroup的dispatchTouchEvent发现mGroupFlags等于FLAG_DISALLOW_INTERCEPT常量,则会走里面view的dispatchTouchEventonTouchEvent方法, 后续就是通过该方法让外层的viewpager2不拦截里面的列表页的recyclerview。如果不拦截的话,就会触发列表的上下滑动,而不会出现横向的viewpager2滑动。 上面我们已经知道viewpager中的RecyclerViewImpl是一个recyclerview,所以我们根据viewgroup到view的事件传递可以知道,当上下滑动列表的recyclerview的时候,会先触发外层的viewpager2-->RecyclerViewImpl-->Recyclerview的dispatchTouchEvent事件,而在Recyclerview中没有重写dispatchTouchEvent事件,而重写了onInterceptTouchEvent事件,在该事件中拦截与否是通过判断mScrollState == SCROLL_STATE_DRAGGING来确定的,而在action_down中默认是不会设置mScrollState == SCROLL_STATE_DRAGGING的,所以排除了action_down中不拦截事件到列表的recyclerview中,在action_move过程中通过判断滑动的dx或dy是否大于mTouchSlop临界值来是否要拦截事件到列表的recyclerview中,所以我们想象下,在上下滑动的时候,肯定会在action_move过程中dx或dy是要大于mTouchSlop的,所以会导致viewpager2出现了横向切换tab了。

既然知道了是在action_move过程中是因为dx或dy大于了mTouchSlop,导致viewpager2中拦截了touch事件,所以我们有没有办法让上下滑动列表recyclerview的时候,不去拦截touch事件呢,答案就是通过上面的requestDisallowInterceptTouchEvent方法,告诉viewpager2不要去拦截我(列表recyclerview)的touch事件,让我能顺利去完成上下滑动。

既然想到了办法,那么下面来重写列表页(我这里用的是xrecyclerview作为列表)的,因为有上拉和下拉。我这里先直接上代码,处理这种滑动冲突问题:

public class RecyclerViewAtViewPager2 extends XRecyclerView {

    public RecyclerViewAtViewPager2(@NonNull Context context) {
        super(context);
    }

    public RecyclerViewAtViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RecyclerViewAtViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private int startX, startY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) ev.getX();
                startY = (int) ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);//告诉viewgroup不要去拦截我
                break;
            case MotionEvent.ACTION_MOVE:
                int endX = (int) ev.getX();
                int endY = (int) ev.getY();
                int disX = Math.abs(endX - startX);
                int disY = Math.abs(endY - startY);
                if (disX > disY) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);//下拉的时候是false
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

}

上面代码主要是在action_move过程中判断纵向的坐标变化量是否大于横向的坐标变化量来确定告诉父类不拦截我的touch事件,如果是横向的坐标变化量大于纵向的坐标变化量则让父类处理touch事件,此时会去拦截我的touch事件。

我们通过举一反三,如果列表页不是recyclerview呢,而是一个nestscrollview的话,也是可以重写nestscrollview的dispatchTouchEvent方法处理类似的问题,所以在出现类似的冲突问题情况下,通过判断两个方向的偏移量来确定是否要拦截我的touch事件。

好了,本篇代码其实不多,主要讲到了requestDisallowInterceptTouchEvent方法如何处理是否要拦截子view的分发事件。希望能帮到大家在滑动页面既要横向滑动和纵向滑动的时候冲突问题,有什么问题直接留言!!!