RecyclerView内嵌ViewPager无限滑动Banner的爬坑之旅
前言收到线上用户反馈,RecyclerView 实现的 Feed 流列表中的 Banner Item 在滑动过程中偶现没有进行内容切换,而是进行了外层频道切换。嵌套的UI布局如下图所示: 问题原因定位猜测原因是:最外层OuterViewPager拦截了Touch事件,没有将Touch事件传递给内层的BannerViewPager,从而导致外层频道切换。 想证实猜测的准确性,定位为什么OuterViewPager拦截了事件,只能通过阅读ViewPager的事件拦截源码进行分析,这是最快也是最靠谱的证实方案。 ViewPager事件拦截原理从onInterceptTouchEvent源码分析一下ViewPager对Touch事件的拦截机制,相关源码已经添加中文注解: @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // cancel和up事件代表触摸事件结束,需要重置触摸变量 resetTouch(); return false; } if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) { // 如果ViewPager已经响应拖拽事件,则直接拦截后续事件 return true; } if (mIsUnableToDrag) { // 如果ViewPager不能响应拖拽事件,则不拦截后续事件 return false; } } switch (action) { case MotionEvent.ACTION_MOVE: { // 多指触摸处理,值得学习阅读 final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { 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); // 这里是关键,判断OuterViewPager是否需要将touch事件传递给内层BannerViewPager if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // 如果内层Child可以滑动,则OuterViewPager不拦截事件,将事件向下传递 mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } // OuterViewPager开始接管Touch事件处理. // X轴横向偏移量大于最小滑动距离,并且滑动角度小于45度 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { // 设置拦截拖拽标记位 mIsBeingDragged = true; // 通知父View不要拦截事件 requestParentDisallowInterceptTouchEvent(true); // 设置滑动状态为开始拖拽 setScrollState(SCROLL_STATE_DRAGGING); // 设置滑动开始的坐标 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { // 竖向滑动不拦截后续TOUCH事件 mIsUnableToDrag = true; } if (mIsBeingDragged) { // 执行滑动 if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } case MotionEvent.ACTION_DOWN: { // 多指处理的逻辑,值得学习,标准写法 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) { // down事件到来,需要终止上次的滑动 mScroller.abortAnimation(); mPopulatePending = false; populate(); // 因为上次滑动没有终止,因此需要拦截后续TOUCH事件,开始新的滑动 mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } else { completeScroll(false); mIsBeingDragged = false; } break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } // 速度追踪 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); return mIsBeingDragged;}通过onInterceptTouchEvent源码分析,可以看出: ...