Android:解决ViewPager和(RecyclerView、HorizontalScrollView)内部横向滚动控件的触摸滑动冲突

其实ViewPager对于触摸事件的分发已经做得非常好了,HorizontalScrollView以及使用了横向LinearLayoutManager的RecyclerView或者某些第三方banner轮播控件,基本没什么问题,能滚动到最后的,才会触发ViewPager的横向切换滑动。

但是在某些情况下,比如我这边使用场景是多个菜单栏,使用了第三方类似ViewPager的LayoutManager:PagerGridLayoutManager(https://xiaozhuanlan.com/topic/5841730926) 这个功能确实厉害。解决了List数据需要分页滑动显示问题。使用了这个的话,触摸就不太灵活了,必须是完全横向才能触发RecyclerView,不然触摸就会被ViewPager消费掉。

现在想要实现的是:触摸在菜单栏的RecyclerView上时,不会触发ViewPager的横向滚动。

在这之前,先看看这个问题普遍的解决办法:

1、把ViewPager设置成不响应滑动触摸,它就是一个纯粹的容器,也很容易实现:不拦截触摸,不消费触摸(public boolean onTouchEvent(MotionEvent ev) { return false; }    public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }   ) 很遗憾,这个办法被产品否决了。

2、使用NestedScrolling,ViewPager实现NestedScrollingParent。但是这个实现起来是有2个难点:NestedScrolling的2个接口Parent和Child是说,Child要滑动的时候,都会把自己的行动告诉Parent,Parent来判断是否需要消费,消费多少触摸距离。但是ViewPager是直接就拦截了触摸(之所以这样说,看底下),根本到不了底下的View。因此ViewPager的onInterceptTouchEvent需要大改,然后在onNestedPreScroll来判断是否消费滑动;第二个难点是对于我来说的,我的菜单栏是作为一个header放在另一个垂直滑动的RecyclerView内的,没试过套2层实现NestedScrolling,应该会有很多问题。

3、重写ViewPager的触摸了

本篇算是采用了第三种方法,但是比较简单。

理解这篇文章的前提,是需要对触摸事件分发有一点小小的了解。自上而下 自下而上    当前View onInterceptTouchEvent返回false的时候,事件继续向下传递,返回true,事件到当前view的onTouchEvent处理,onTouchEvent返回false则往上返回。因此假如一个ViewGroup包裹一个View 然后不设置onClick之类的,触摸View,事件从activity -> ViewGoup -> View ->  ViewGoup ->activity  这块不深究的话比较简单。

先写一下开始的思路:开始猜测的是,底层View的问题,也就是上面的RecyclerView。当然,其他控件都没问题,单单使用了PagerGridLayoutManager就有问题,肯定是PagerGridLayoutManager和ViewPager有冲突。因此我的目光看到了最下层的RecyclerView(下面用rvMenu代替)上,我的想法是rvMenu没完全消费完触摸事件,然后就到了上层的ViewPager上,因此写了个TouchView用来包裹rvMenu:

public class TouchView extends LinearLayout {
    public TouchView(Context context) {
        super(context);
    }

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }
}

不拦截触摸事件,因此触摸事件会继续往下传递,onTouchEvent返回true 把触摸事件都消费掉,这样ViewPager就接收不到触摸事件了(说一句,不要去动RecyclerView的触摸事件,因为它还是要传递给子View的,不能简单的返回true或者false)。想法很美好,结果并没有什么改变。因此可以得出上面说的结论:ViewPager是直接就拦截了触摸然后进行滑动

现在目光就转到了ViewPager上了:现在的思路是,当我触摸在rvMenu上时,onInterceptTouchEvent返回false,触摸其他位置时,默认不作处理,那么整体框架就出来了:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
       
        if (是rvMenu的话) { 
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

其实文章到这里就算结束了,判断返回false的时机,看具体情况。假如你是需要某个特定的ViewPager下的包裹的东西不响应的话,比如说在CFragment的时候横向滑动不响应,就可以这样写:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Fragment fragment = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem());
        if (fragment instanceof CFragment) { 
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

我的情况是子View的子View,因此采用的是,MotionEvent的getRawX()和getRawY()来对比rvMenu的位置,如果是处于rvMenu的位置的话,返回false:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        View v =  ((ViewAdapter) getAdapter()).getItem(getCurrentItem());
        if (v instanceof MainFeedView) {
            if (((MainFeedView) v).isOnTouch((int)ev.getRawX(), (int)ev.getRawY())) {
                return false;
            } else {
                return super.onInterceptTouchEvent(ev);
            }
        }
        return super.onInterceptTouchEvent(ev);
    }
}


//MainFeedView里的isOnTouch方法:
public boolean isOnTouch(int x, int y) {
        return isTouchPointInView(rvMenu, x, y);
    }


//(x,y)是否在view的区域内
    public static boolean isTouchPointInView(View view, int x, int y) {
        if (view == null) {
            return false;
        }
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int left = location[0];
        int top = location[1];
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        //view.isClickable() &&
        if (y >= top && y <= bottom && x >= left
                && x <= right) {
            return true;
        }
        return false;
    }

猜你喜欢

转载自blog.csdn.net/qq_27454233/article/details/112787617