美文网首页RecyclerView系列Android-recyclerview
RecyclerView的onTouchEvent源码浅析

RecyclerView的onTouchEvent源码浅析

作者: isLJli | 来源:发表于2020-11-25 11:35 被阅读0次

从哪里找RecyclerView滑动事件处理?

我们都知道RecyclerView是一个可以滑动的ViewGroup,而且还是一个实现NestedScrollingChild2、3可以嵌套滑动ViewGroup。那么我们的手指在手机点击滑动时,RecyclerView究竟是怎么做的呢?

由如下图的ViewGroup事件分发机制可以得出,ViewGroup手势事件只能在三个方法中:1.在分发事件的方法dispatchTouchEvent;2.是在拦截事件:onInterceprTouchEvent;3.在onTouchEvent方法中。第一个dispatchTouchEventRecyclerView并没有重写,而是复用了ViewGroup本省的dispatchTouchEvent方法,所以并没有在dispatchTouchEvent处理手势事件。第二个onInterceprTouchEvent拦截方法也只是在DownUp做一些嵌套滑动开始结束查询操作,在MOVE事件并没有什么事情。所以第三个onTouchEvent就是RecyclerView手势滑动事件的方法。

ViewGroup的事件分发

onTouchEvent方法处理

  @Override
  public boolean onTouchEvent(MotionEvent e) {
      if (mLayoutSuppressed || mIgnoreMotionEventTillDown) {
          return false;
      }
      // RecyclerView提供了一个OnItemTouchListener接口,可以让你自己重写onInterceptTouchEvent,onTouchEvent,
      //onRequestDisallowInterceptTouchEvent方法,那么这时就会调用你自定义接口onTouchEvent方法
      if (dispatchToOnItemTouchListeners(e)) {
          cancelScroll();
          return true;
      }

      if (mLayout == null) {
          return false;
      }

      //向LayoutManager询问是哪边可滑动
      final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
      final boolean canScrollVertically = mLayout.canScrollVertically();

      // 这个根据fing力度算可滑动距离
      if (mVelocityTracker == null) {
          mVelocityTracker = VelocityTracker.obtain();
      }
      boolean eventAddedToVelocityTracker = false;

      final int action = e.getActionMasked();
      final int actionIndex = e.getActionIndex();

      if (action == MotionEvent.ACTION_DOWN) {
          mNestedOffsets[0] = mNestedOffsets[1] = 0;
      }
      final MotionEvent vtev = MotionEvent.obtain(e);
      vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

      switch (action) {
           //手指点击屏幕时
          case MotionEvent.ACTION_DOWN: {
              //跟新 id和开始点击位置
              mScrollPointerId = e.getPointerId(0);
              mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
              mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

              // 设置滑动方向的变量,查询是否有配合滑动的父View
              int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
              if (canScrollHorizontally) {
                  nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
              }
              if (canScrollVertically) {
                  nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
              }
              startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
          }
          break;

          // 多指按下
          case MotionEvent.ACTION_POINTER_DOWN: {
              // 跟新最后手指的 id和初始位置
              mScrollPointerId = e.getPointerId(actionIndex);
              mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
              mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
          }
          break;

          // 手指在屏幕滑动
          case MotionEvent.ACTION_MOVE: {
              final int index = e.findPointerIndex(mScrollPointerId);
              if (index < 0) {
                  Log.e(TAG, "Error processing scroll; pointer index for id "
                          + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                  return false;
              }

              // 算出手指滑动的距离
              final int x = (int) (e.getX(index) + 0.5f);
              final int y = (int) (e.getY(index) + 0.5f);
              int dx = mLastTouchX - x;
              int dy = mLastTouchY - y;

              // 手指滑动距离是否比系统默认的最小滑动距离大,设置是否可滑动
              if (mScrollState != SCROLL_STATE_DRAGGING) {
                  boolean startScroll = false;
                  if (canScrollHorizontally) {
                      if (dx > 0) {
                          dx = Math.max(0, dx - mTouchSlop);
                      } else {
                          dx = Math.min(0, dx + mTouchSlop);
                      }
                      if (dx != 0) {
                          startScroll = true;
                      }
                  }
                  if (canScrollVertically) {
                      if (dy > 0) {
                          dy = Math.max(0, dy - mTouchSlop);
                      } else {
                          dy = Math.min(0, dy + mTouchSlop);
                      }
                      if (dy != 0) {
                          startScroll = true;
                      }
                  }
                  if (startScroll) {
                      setScrollState(SCROLL_STATE_DRAGGING);
                  }
              }

              if (mScrollState == SCROLL_STATE_DRAGGING) {
                  mReusableIntPair[0] = 0;
                  mReusableIntPair[1] = 0;
                  // 先给配合嵌套父view滑动
                  if (dispatchNestedPreScroll(
                          canScrollHorizontally ? dx : 0,
                          canScrollVertically ? dy : 0,
                          mReusableIntPair, mScrollOffset, TYPE_TOUCH
                  )) {
                      // 手指滑动的距离减去父View消耗的距离
                      dx -= mReusableIntPair[0];
                      dy -= mReusableIntPair[1];
                      // Updated the nested offsets
                      mNestedOffsets[0] += mScrollOffset[0];
                      mNestedOffsets[1] += mScrollOffset[1];
                      // Scroll has initiated, prevent parents from intercepting
                      getParent().requestDisallowInterceptTouchEvent(true);
                  }

                  mLastTouchX = x - mScrollOffset[0];
                  mLastTouchY = y - mScrollOffset[1];
                 // recylcerView将手指滑动的距离自己进行消耗滑动
                  if (scrollByInternal(
                          canScrollHorizontally ? dx : 0,
                          canScrollVertically ? dy : 0,
                          e, TYPE_TOUCH)) {
                      getParent().requestDisallowInterceptTouchEvent(true);
                  }
                  if (mGapWorker != null && (dx != 0 || dy != 0)) {
                      // 尝试拿到ViewHolder
                      mGapWorker.postFromTraversal(this, dx, dy);
                  }
              }
          }
          break;

          case MotionEvent.ACTION_POINTER_UP: {
             // 跟新id和初始位置
              onPointerUp(e);
          }
          break;

          case MotionEvent.ACTION_UP: {
             
              mVelocityTracker.addMovement(vtev);
              eventAddedToVelocityTracker = true;
              mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
               // 根据速度算出距离
              final float xvel = canScrollHorizontally
                      ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
              final float yvel = canScrollVertically
                      ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
              // 开始fling距离
              if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                  setScrollState(SCROLL_STATE_IDLE);
              }
              resetScroll();
          }
          break;

          case MotionEvent.ACTION_CANCEL: {
              cancelScroll();
          }
          break;
      }

      if (!eventAddedToVelocityTracker) {
          mVelocityTracker.addMovement(vtev);
      }
      vtev.recycle();

      return true;
  }

在研究onTouchEvent()方法执行过程中,先不深究每个方法的具体实现,而是整个流程的大致做了什么。

  1. 手指按下: 更新触摸id和初始位置,与查询是否有父View配合嵌套滑动。
  2. 多指按下:根据最后的手指跟新触摸id和初始位置。
  3. 手指滑动: 得到手指滑动的距离,然后判断这个距离是否达到滑动大小。将手指滑动距离先交给父View消耗dispatchNestedPreScroll,如果父View消耗了,则手指滑动的距离减去父view消耗的距离,再将滑动的距离交给RV滑动消耗scrollByInternal,RV消耗完,再询问父View是否要消耗。并且尝试拿到ViewHoldermGapWorker.postFromTraversal
  4. 多指离开:跟新触摸id和初始位置。
  5. 单指离开: 根据Fling的力度算出可滑动的距离,然后调用fling方法进行滑动。

这里有三个比较重要方法:分别是MOVE时的RV滑动方法scrollByInternal,获取ViewHolder的mGapWorker.postFromTraversal。最后就是执行UP时的fling方法。

scrollByInternal

  boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
      int unconsumedX = 0;
      int unconsumedY = 0;
      int consumedX = 0;
      int consumedY = 0;

      consumePendingUpdateOperations();
      if (mAdapter != null) {
          mReusableIntPair[0] = 0;
          mReusableIntPair[1] = 0;
          // rv滑动的方法
          scrollStep(x, y, mReusableIntPair);
          consumedX = mReusableIntPair[0];
          consumedY = mReusableIntPair[1];
          unconsumedX = x - consumedX;
          unconsumedY = y - consumedY;
      }
      if (!mItemDecorations.isEmpty()) {
          invalidate();
      }

      mReusableIntPair[0] = 0;
      mReusableIntPair[1] = 0;
      //询问父view是否要消耗
      dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
              type, mReusableIntPair);
      unconsumedX -= mReusableIntPair[0];
      unconsumedY -= mReusableIntPair[1];
       // 父View是否有消耗
      boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

      // Update the last touch co-ords, taking any scroll offset into account
      mLastTouchX -= mScrollOffset[0];
      mLastTouchY -= mScrollOffset[1];
      mNestedOffsets[0] += mScrollOffset[0];
      mNestedOffsets[1] += mScrollOffset[1];

      if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
          if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
              pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
          }
          considerReleasingGlowsOnScroll(x, y);
      }
      if (consumedX != 0 || consumedY != 0) {
          dispatchOnScrolled(consumedX, consumedY);
      }
      if (!awakenScrollBars()) {
          invalidate();
      }
      return consumedNestedScroll || consumedX != 0 || consumedY != 0;
  }
    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();

        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);

        int consumedX = 0;
        int consumedY = 0;
        // 调用LayoutManager的scrollHorizontallyBy方法来给RV滑动
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

        TraceCompat.endSection();
        repositionShadowingViews();

        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);

        if (consumed != null) {
            consumed[0] = consumedX;
            consumed[1] = consumedY;
        }
    }

scrollByInternal 方法先是调用LayoutManager让RV滑动消耗距离,然后再将剩余的距离询问父View是否需要消耗。

相关文章

网友评论

    本文标题:RecyclerView的onTouchEvent源码浅析

    本文链接:https://www.haomeiwen.com/subject/tdajbktx.html