项目中用到了 RecycleView 嵌套,水平方向嵌套竖直方向的,结果发现一个问题,就是水平方向滑动后,直接竖直滑动,往往第一下竖直方向没响应,要等到第二下甚至第三下竖直方向的 RecycleView 才能响应滑动事件,可是我们交互要求如果正在水平滑动,突然竖直滑动,竖直方向的 RecycleView 是要立即响应的,这就有点意思了,为啥会出现这种情况呢,查看源码才知道,在 RecycleView 的 onInterceptTouchEvent() 中做了处理

RecyclerView.java

public boolean onInterceptTouchEvent(MotionEvent e) {
   ...
   switch (action) {
       case MotionEvent.ACTION_DOWN:

       if (mScrollState == SCROLL_STATE_SETTLING) {
           getParent().requestDisallowInterceptTouchEvent(true);
           setScrollState(SCROLL_STATE_DRAGGING);
       }
       ...
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
 }

当处于滑行状态,即快速滑动后到滑动静止之间的时候,会在 DOWN 事件的时候拦截掉这个事件

外部的 RecycleView 拦截后,那么内部的 RecycleView 就收不到 Touch 事件,也就没办法响应滑动事件了。

而且我们知道,一旦 DOWN 事件中被拦截,那么剩下的 MOVE 和 UP 等事件,也都会直接传递给该 View 。

也就是说一旦外部的 RecycleView 在拦截了 DOWN 事件,那么就剩下一系列事件就别想传递到内部的 RecycleView 了,内部的 RecycleView 接收不到事件,就没办法滑动了。

所以第一步就是不能让外部 RecycleView 给拦截了,起码不能拦截竖直方向的。 然后我又做了一些处理,当滑动的时候,外部 RecycleView 只拦截水平方向的滑动事件,具体代码如下。

public class OutRecyclerView extends RecyclerView {
    private static final String TAG = "OutRecyclerView";
    private int mInitialTouchX;
    private int mInitialTouchY;

    private static final int INVALID_POINTER = -1;
    private int mScrollPointerId = INVALID_POINTER;

    private int mTouchSlop;
    private ViewConfiguration mViewConfiguration;

    public OutRecyclerView(Context context) {
        this(context, null);
    }

    public OutRecyclerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs , 0);
    }

    public OutRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs , defStyle);
        mViewConfiguration = ViewConfiguration.get(context);
        mTouchSlop = mViewConfiguration.getScaledTouchSlop();
    }

    @Override
    public void setScrollingTouchSlop(int slopConstant) {
        super.setScrollingTouchSlop(slopConstant);
        switch (slopConstant) {
            case TOUCH_SLOP_DEFAULT:
                mTouchSlop = mViewConfiguration.getScaledTouchSlop();
                break;
            case TOUCH_SLOP_PAGING:
                mTouchSlop = mViewConfiguration.getScaledPagingTouchSlop();
                break;
            default:
                break;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        final int action = e.getAction();
        boolean intercepted = false;
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
                if (index < 0) {
                    return false;
                }
                final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                final int dx = x - mInitialTouchX;
                final int dy = y - mInitialTouchY;
                final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
                boolean startScroll = false;
                //可以水平方向滑动并且滑动距离大于 mToushSLop ,并且水平方向滑动距离大于竖直方向的。
                if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy))) {
                    startScroll = true;//水平方向
                }
                intercepted = super.onInterceptTouchEvent(e);
                //如果竖直方向,那么 startScroll 为 false ,
                //那么不管super.onInterceptTouchEvent(e)true或者 false ,结果都是 false ,那么就不会拦截。
                return startScroll && intercepted;
            }
            default:
                int state = getScrollState();
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = (int) (e.getY() + 0.5f);
                intercepted = super.onInterceptTouchEvent(e);
                if (state == SCROLL_STATE_SETTLING || state == SCROLL_STATE_DRAGGING) {
                    intercepted = false;
                }
                return intercepted;
        }
    }

这样就保证在 DOWN 事件时候,外部 RecycleView 不会拦截掉,并且只拦截水平方向的事件, 这样就可以了吗,其实还不行,因为这样会有另外一个问题,尽管内部的 RecycleView 接收到了事件,可是当内部的 RecycleView 竖直滑动中,如果这个时候再水平滑动,那么外部 RecycleView 又接收不到事件了,因为事件已经传递给了内部 RecycleView ,这个时候,就需要进行判断,如果是水平滑动,那么应该把事件交由外部的 RecycleView ,然后就想起来 requestDisallowInterceptTouchEvent() 这个,在 dispatchTouchEvent() 中判断滑动速度,如果是水平方向大于竖直方向,那么就交给外部的 RecycleView 处理。即

public class InRecyclerView extends RecyclerView {
    private static final String TAG = "HomePageRecyclerView";
    //速度追踪
    private VelocityTracker mVelocityTracker;

    private int mInitialTouchX;
    private int mInitialTouchY;

    //是否正在快速滑动
    public boolean mIsFastScrolling = false;
    //判定为快速滑动的速度阀值
    private static final int SCROLL_THRESHOLD_SPEED = 6000;

    public InRecyclerView(Context context) {
        this(context, null);
    }

    public InRecyclerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs , 0);
    }

    public InRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs , defStyle);
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mInitialTouchX = (int) (ev.getX() + 0.5f);
                mInitialTouchY = (int) (ev.getY() + 0.5f);
                break;
            case MotionEvent.ACTION_MOVE:
                //向 velocityTracker 对象添加action
                mVelocityTracker.addMovement(ev);
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                float yVelocity = mVelocityTracker.getYVelocity();
                final int x = (int) (ev.getX() + 0.5f);
                final int y = (int) (ev.getY() + 0.5f);
                final int dx = x - mInitialTouchX;
                final int dy = y - mInitialTouchY;
                //SCROLL_STATE_SETTLING:滑动后自然沉降的状态 SCROLL_STATE_DRAGGING 滑动状态
                if ((getScrollState() == SCROLL_STATE_SETTLING || getScrollState() == SCROLL_STATE_DRAGGING)
                        && (Math.abs(xVelocity) > Math.abs(yVelocity)) && (Math.abs(dx) > Math.abs(dy))) {
                    //水平方向滑动的速度和距离都大于竖直方向,告诉父 View ,这种情况下,我不需要,事件交给父View
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
                if (Math.abs(yVelocity) > SCROLL_THRESHOLD_SPEED) {
                    mIsFastScrolling = true;
                }
                break;
            default:
        }
        return super.dispatchTouchEvent(ev);
    }    
}

搬运地址: