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

解决了什么问题?

  1. 竖向RecyclerView嵌套横向RecyclerView时的滑动冲突怎么解决?
  2. 竖向RecyclerView嵌套横向RecyclerView时以45度分开处理?

问题描述

我们写瀑布流是,如果竖向RecyclerView嵌套横向RecyclerView,当滑动横向RecyclerView时,竖向的RecyclerView会抖动。这是为什么呢?要分析这个问题我们首先需要了解事件分发机制。如果你已经熟知这一部分可以跳过。

什么是事件分发?

简单来说,事件分发就是用户手点击屏幕之后,点击信息的传递过程。

谁处理事件分发?

答案是Activity里的PhoneWindow,要了解PhoneWindow就要先看看Activity的构成。

  • Activity
    • PhoneWindow
      • DecorView
        • TitleView
        • ContentView

以上就是Activity的构成结构图,要知道PhoneWindow是属于Activity下的一层视图即可。

怎么简单理解事件分发?

要了解事件分发我们要首先看一段伪代码。

	public boolen dispatchTouchEvent(MotionEvent ev){
		boolen result  = false;
		if(onInterceptTouchEvent(ev)){
			result = onTouchEvent(ev);
		}else{
			result = child.dispatchTouchEvent(ev);
		}
	}
  • onInterceptTouchEvent和OnTouchEvent都是在dispatchTouchEvent方法里调用的。如果在不重写dispatchTouchEvent的方法前提下,这段代码已经可以解释事件分发了,如果onInterceptTouchEvent(ev)返回true就调用ViewGroup自身的onTouchEvent方法,如果是false就调用子控件的dispatchTouchEvent方法。
  • 如果重写了dispatchTouchEvent方法,显然以上代码的方法就不会执行,则事件会交给父View的onTouchEvent执行。
  • 如果dispatchTouchEvent和onInterceptTouchEvent都不重写,则会向下传递到最后一个子View的onTouchEvent方法,这时事件就会向上传递给父控件的onTouchEvent方法,直到遇到第一个返回为true的控件后方法结束。
  • 这里需要注意的是View中是没有onInterceptTouchEvent()方法的,只有ViewGroup才有。

事件分发总结

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一样

问题分析

了解了事件分发,我们来分析这个问题,如图所示
Android 解决竖向RecyclerView嵌套横向RecyclerView时的滑动冲突_第1张图片
在滑动横向RecyclerView时事件会从竖向的RecyclerView里传过来,当我们滑动的手势触发了竖向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进行处理,反之亦然。

源码

我给它起了一个名字叫BetterGestureRecyclerView

你可能感兴趣的:(Android)