ViewPager嵌套下的RecycleView滑动冲突

起因

主页的某一个Tab有是一个类似与新闻客户端的多tab页面,如
QQ图片20180724175629.png

在用户要滑动的Banner活动图的时候,时而会触发切换tab,时而会触发切换banner导致操作上的一种不适感

现象大致分析

  • 即使不看源码,凭感觉也可以知道肯定是ViewPager的在处理Move行为的时候没有判断好(因为down的时候肯定是不知道让哪一个组件来处理的)

需要达到的效果

  • 在Banner处滑动的时候,交由Banner来处理
  • 在Banner下方列表处上下滑动交由该tab下的recycleview来处理(也就是不需要任何改动)
  • 在Banner下方列表处左右滑动交由ViewPager来处理

源码分析

我们直接来看ViewPager的onInterceptTouchEvent方法,毕竟是这个方法来处理事件的拦截与否

   switch (action) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                 * whether the user has moved far enough from his original down touch.
                 */

                /*
                * Locally do absolute value. mLastMotionY is set to the y value
                * of the down event.
                */
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }

                final int pointerIndex = ev.findPointerIndex(activePointerId);
                final float x = ev.getX(pointerIndex);
                final float dx = x - mLastMotionX;
                final float xDiff = Math.abs(dx);
                final float y = ev.getY(pointerIndex);
                final float yDiff = Math.abs(y - mInitialMotionY);
                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
                        && canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0
                            ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                    // The finger has moved enough in the vertical
                    // direction to be counted as a drag...  abort
                    // any attempt to drag horizontally, to work correctly
                    // with children that have scrolling containers.
                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = ev.getPointerId(0);
                mIsUnableToDrag = false;

                mIsScrollStarted = true;
                mScroller.computeScrollOffset();
                if (mScrollState == SCROLL_STATE_SETTLING
                        && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                    // Let the user 'catch' the pager as it animates.
                    mScroller.abortAnimation();
                    mPopulatePending = false;
                    populate();
                    mIsBeingDragged = true;
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                } else {
                    completeScroll(false);
                    mIsBeingDragged = false;
                }

                if (DEBUG) {
                    Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                            + " mIsBeingDragged=" + mIsBeingDragged
                            + "mIsUnableToDrag=" + mIsUnableToDrag);
                }
                break;
            }

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        /*
         * The only time we want to intercept motion events is if we are in the
         * drag mode.
         */
        return mIsBeingDragged;
    }

返回值为mIsBeingDragged,并且上文说了只需要看Move的行为,那么可以得知我们只需要看ViewPager什么时候返回了false(也就是你不要拦我啊,我要这个行为给我的孩子)

仅需要看这一段

 if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
                        && canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }
  • dx!=0不需要关注,必然符合
  • isGutterDrag表示是否是在ViewPager的缝隙处滑动,不需要关注
  • 重点来了,就是这个canScroll方法
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
                        && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
                        && canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    }

紧接着

  • 可以看到,这个方法的目的就是在于遍历子View,孙View等里是否有可以横向滚动的组件并且落点在组件内部的
  • 关键方法ViewCompat.canScrollHorizontally,按理来说RecycleView设置了横向的LinearLayoutManager,应该是没问题才对
  • 进到RecycleView源码可知,并没有重写canScrollHorizontally,而是在合适的时候(比如onTouch)返回了mLayout.canScrollHorizontally()

处理方案

  • 方案一
    重写RecycleView的canScrollHorizontally,直接返回为true

经测试,有效

  • 方案二
    手动处置ViewPager的onInterceptTouchEvent,添加监听,在监听中判断落点是否在Banner的组件里,并且在RecycleView重写dispatchTouchEvent
   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mMoveListener == null ? !noScroller && super.onInterceptTouchEvent(ev) : mMoveListener.onMoveTouchEvent(ev) ? super.onInterceptTouchEvent(ev) : false;
    }

    private OnMoveTouchListener mMoveListener;

    public void setOnMoveListener(OnMoveTouchListener l) {
        mMoveListener = l;
    }

    public interface OnMoveTouchListener {
        boolean onMoveTouchEvent(MotionEvent ev);
    }
 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getY() > getMeasuredHeight())
            return false;
        return super.dispatchTouchEvent(ev);
    }
  • 还没完,还需要判断当前界面是否显示
  @Override
    public boolean onMoveTouchEvent(MotionEvent ev) {
        boolean b = getFragmentVisible() ? !mModulePage.getBannerPager().dispatchTouchEvent(ev) : false;
         return b;
      }

总之非常的麻烦,效果也能达到,还是方案一好,完结

你可能感兴趣的:(ViewPager嵌套下的RecycleView滑动冲突)