解决竖向RecyclerView嵌套横向RecyclerView时的滑动冲突

问题描述

我们写瀑布流是,如果竖向RecyclerView嵌套横向RecyclerView,当滑动横向RecyclerView时,竖向的RecyclerView会抖动。

事件分发总结

dispatchTouchEvent

return true:表示该View内部消化掉了所有事件
return false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费
return super.dispatchTouchEvent(ev):默认事件将分发给本层的事件拦截onInterceptTouchEvent方法进行处理

onInterceptTouchEvent

return true:表示将事件进行拦截,并将拦截到的事件交由本层控件的onTouchEvent进行处理
return false:表示不对事件进行拦截,事件得以成功分发到子View
return super.onInterceptTouchEvent(ev):默认表示不拦截该事件,并将事件传递给下一层View的dispatchTouchEvent

onTouchEvent

return true:表示onTouchEvent处理完事件后消费了此次事件
return fasle:表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true
return super.dispatchTouchEvent(ev):表示不响应事件,结果与return false一样

问题分析

在滑动横向RecyclerView时事件会从竖向的RecyclerView里传过来,当我们滑动的手势触发了竖向RecyclerView的滑动事件的时候,事件就会被拦截,这样横向的RecyclerView就不会滑动,而竖向的的RecyclerView就会上下抖动。

RecyclerView滑动触发部分源码 

public boolean onInterceptTouchEvent(MotionEvent e) {
        if (this.mLayoutFrozen) {
            return false;
        } else if (this.dispatchOnItemTouchIntercept(e)) {
            this.cancelTouch();
            return true;
        } else if (this.mLayout == null) {
            return false;
        } else {
            boolean canScrollHorizontally = this.mLayout.canScrollHorizontally();
            boolean canScrollVertically = this.mLayout.canScrollVertically();
            if (this.mVelocityTracker == null) {
                this.mVelocityTracker = VelocityTracker.obtain();
            }

            this.mVelocityTracker.addMovement(e);
            int action = e.getActionMasked();
            int actionIndex = e.getActionIndex();
            switch(action) {
            case 0:
               ...
            case 1:
               ...
            //从这里开始
            case 2://这里的2 为 ACTION_MOVE = 2
                int index = e.findPointerIndex(this.mScrollPointerId);
                if (index < 0) {
                    Log.e("RecyclerView", "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                int x = (int)(e.getX(index) + 0.5F);
                int y = (int)(e.getY(index) + 0.5F);
                if (this.mScrollState != 1) {
                    int dx = x - this.mInitialTouchX;
                    int dy = y - this.mInitialTouchY;
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
                        this.mLastTouchX = x;
                        startScroll = true;
                    }

                    if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
                        this.mLastTouchY = y;
                        startScroll = true;
                    }

                    if (startScroll) {
                        this.setScrollState(1);
                    }
                }
                break;
            //到这里结束
            case 3:
               ...
            }
            return this.mScrollState == 1;
        }
    }

看上面的RecyclerView源码可知,当

if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
                        this.mLastTouchX = x;
                        startScroll = true;
                    }

if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
                        this.mLastTouchY = y;
                        startScroll = true;
                    }

这两个条件成立时,startScroll就会被设置为true,然后调用this.setScrollState(1);


void setScrollState(int state) {
        if (state != this.mScrollState) {//mScrollState默认值为0
            this.mScrollState = state;
            if (state != 2) {
                this.stopScrollersInternal();
            }

            this.dispatchOnScrollStateChanged(state);
        }
    }

在这里把mScroState的默认值设置为了1,最后onInterceptTouchEvent返回了

return this.mScrollState == 1;

也就是true。了解了滑动触发的源码我们就在这里对RecyclerView进行修改即可。
如何修改

我们再来看看触发RecyclerView滑动方法的条件

if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
                        this.mLastTouchX = x;
                        startScroll = true;
                    }

if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
                        this.mLastTouchY = y;
                        startScroll = true;
                    }

条件1:当可以横向滑动时,且横向滑动距离的绝对值大于触发滑动的阈值mTouchSlop触发
条件2:当可以纵向滑动时,且纵向滑动距离的绝对值大于触发滑动的阈值mTouchSlop触发
问题在哪?

问题就在于只要滑动的距离绝对值大于阈值即可。结合我们的例子,外面的纵向RecyclerView接收到的滑动只要纵向滑动的距离分量绝对值大于阈值mTouchSlop就会触发第二个条件返回true,进行拦截。
即使用户横向滑动的距离分量大于纵向也不会交给横向的RecyclerView处理,这样就会发生纵向RecyclerView抖动的问题
如何解决
知道了问题所在,我们只要加上如下这个判断即可

if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop
&& Math.abs(dx) > Math.abs(dy)) {
                        startScroll = true;
                    }

if (canScrollVertically && Math.abs(dy) > this.mTouchSlop
&& Math.abs(dy) > Math.abs(dx)) {
                        startScroll = true;
                    }

横向滑动时判断横向的分量是否大于纵向的,反之亦然。这样就可以实现45度滑动的分隔,用户与水平夹角小于45度滑动时就会交给横向的RecyclerView进行处理,反之亦然。

 附上源码如下:

package com.newsweekly.livepi.mvp.ui.widget;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

public class BetterRecyclerView extends RecyclerView {

    private int mScrollPointerId;
    private int mInitialTouchX, mInitialTouchY;
    private int mTouchSlop;

    public BetterRecyclerView (@NonNull Context context) {
        super(context);
        init();
    }

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

    public BetterRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        ViewConfiguration vc = ViewConfiguration.get(getContext());
        this.mTouchSlop = vc.getScaledTouchSlop();
    }

    @Override
    public void setScrollingTouchSlop(int slopConstant) {
        ViewConfiguration vc = ViewConfiguration.get(this.getContext());
        switch (slopConstant) {
            case 0:
                this.mTouchSlop = vc.getScaledTouchSlop();
            case 1:
                this.mTouchSlop = vc.getScaledPagingTouchSlop();
                break;
            default:
                Log.w("RecyclerView", "setScrollingTouchSlop(): bad argument constant " + slopConstant + "; using default value");

        }
        super.setScrollingTouchSlop(slopConstant);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
        boolean canScrollVertically = getLayoutManager().canScrollVertically();
        int action = e.getActionMasked();
        int actionIndex = e.getActionIndex();
        switch (action) {
            //ACTION_DOWN
            case 0:
                mScrollPointerId = e.getPointerId(0);
                this.mInitialTouchX = (int) (e.getX() + 0.5F);
                this.mInitialTouchY = (int) (e.getY() + 0.5F);
                return super.onInterceptTouchEvent(e);
            //ACTION_MOVE
            case 2:
                int index = e.findPointerIndex(this.mScrollPointerId);
                if (index < 0) {
                    Log.e("RecyclerView", "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                int x = (int) (e.getX(index) + 0.5F);
                int y = (int) (e.getY(index) + 0.5F);
                if (getScrollState() != 1) {
                    int dx = x - this.mInitialTouchX;
                    int dy = y - this.mInitialTouchY;
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop && Math.abs(dx) > Math.abs(dy)) {
                        startScroll = true;
                    }

                    if (canScrollVertically && Math.abs(dy) > this.mTouchSlop && Math.abs(dy) > Math.abs(dx)) {
                        startScroll = true;
                    }

                    Log.d("MyRecyclerView", "canX:" + canScrollHorizontally + "--canY" + canScrollVertically + "--dx:" + dx + "--dy:" + dy + "--startScorll:" + startScroll + "--mTouchSlop" + mTouchSlop);

                    return startScroll && super.onInterceptTouchEvent(e);
                }
                return super.onInterceptTouchEvent(e);
            //ACTION_POINTER_DOWN
            case 5:
                this.mScrollPointerId = e.getPointerId(actionIndex);
                this.mInitialTouchX = (int) (e.getX(actionIndex) + 0.5F);
                this.mInitialTouchY = (int) (e.getY(actionIndex) + 0.5F);
                return super.onInterceptTouchEvent(e);
        }

        return super.onInterceptTouchEvent(e);

    }
}

猜你喜欢

转载自blog.csdn.net/xifei66/article/details/105551123