NestedScrolling机制解析(二)——NestedScrollView源码

上一篇文章我们介绍了NestedScrollingParent和NestedScrollingChild接口,了解了两个接口里的方法和相互之间的调用关系。这篇我们以NestedScrollView类为例,看先嵌套滚动Parent和Child之前具体是怎么实现的。为啥用NestedScrollView呢,因为这既是一个NestedScrollingParent又是一个NestedScrollingChild,了解了整个类后就了解了整个机制了。

这里就不全部贴出了源码,后面我们一边讲解一边贴出源码。

一、类简介

还是老规矩,我们先看下Google对这个类的介绍

NestedScrollView is just like ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.

就是说NestedScrollViewScrollView类似,是一个支持滚动的控件。此外,它还同时支持作为NestedScrollingParent或者NestedScrollingChild进行嵌套滚动操作。默认是启用嵌套滚动的。

再看下继承关系

 public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
          NestedScrollingChild2, ScrollingView {}

可以看到该类继承自FrameLayout,实现了NestedScrollingParentNestedScrollingChild

ScrollingView接口。所以才具有上诉的特性咯。

另外这里有个一个NestedScrollingChild2,在上篇文章已经提到了,这个其实核心和NestedScrollingChild是一样的,只是在部分方法上面多了一个type字段用于判断而已。基本上就可以直接看成NestedScrollingChild接口。

另外这里说明一下,因为这里重点是研究嵌套机制的,所以并不是所有的源码都有涉及,只介绍与嵌套相关的

二、嵌套滚动流程分析

1、总流程介绍

总的来说Parent和Child之间的相互调用遵循下面的调用关系:

 

2、具体分析

NestedScrollView是一个FrameLayout也就是一个ViewGroup,根据Android的触摸事件分发机制,一般会进入到onInterceptTouchEvent(MotionEvent ev)进行拦截判断。所以我们也就从这里作为分析的入口。

@Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          /*
           * This method JUST determines whether we want to intercept the motion.
           * If we return true, onMotionEvent will be called and we do the actual
           * scrolling there.
           */
  ​
          /*
          * Shortcut the most recurring case: the user is in the dragging
          * state and he is moving his finger.  We want to intercept this
          * motion.
          */
          final int action = ev.getAction();
          //mIsBeingDragged标识当前View是否在移动 这里的意思在移动或者移动事件都进行拦截
          if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
              return true;
          }
  ​
          switch (action & MotionEvent.ACTION_MASK) {
              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);
                  if (pointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + activePointerId
                              + " in onInterceptTouchEvent");
                      break;
                  }
  ​
                  final int y = (int) ev.getY(pointerIndex);
                  final int yDiff = Math.abs(y - mLastMotionY);
                  // 是滑动事件 并且是垂直方向
                  if (yDiff > mTouchSlop
                          && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                      // 进入这里说明是自己想处理的情况了 所以设置mIsBeingDragged 用于拦截事件
                      mIsBeingDragged = true;
                      mLastMotionY = y;
                      initVelocityTrackerIfNotExists();
                      mVelocityTracker.addMovement(ev);
                      mNestedYOffset = 0;
                      final ViewParent parent = getParent();
                      // 因为自己要处理 所以叫Parent不要拦截
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                  }
                  break;
              }
  ​
              case MotionEvent.ACTION_DOWN: {
                  final int y = (int) ev.getY();
                  // 判断是否是在子控件区域
                  if (!inChild((int) ev.getX(), y)) {
                      mIsBeingDragged = false;
                      recycleVelocityTracker();
                      break;
                  }
  ​
                  /*
                   * Remember location of down touch.
                   * ACTION_DOWN always refers to pointer index 0.
                   */
                  // 记录按下的位置
                  mLastMotionY = y;
                  mActivePointerId = ev.getPointerId(0);
  ​
                  initOrResetVelocityTracker();
                  mVelocityTracker.addMovement(ev);
                  /*
                   * If being flinged and user touches the screen, initiate drag;
                   * otherwise don't. mScroller.isFinished should be false when
                   * being flinged. We need to call computeScrollOffset() first so that
                   * isFinished() is correct.
                  */
                  // 如果是在惯性滑动中的点击 交给自己处理
                  mScroller.computeScrollOffset();
                  mIsBeingDragged = !mScroller.isFinished();
                  // 这里就是开始嵌套滑动的地方了
                  startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                  break;
              }
  ​
              case MotionEvent.ACTION_CANCEL:
              case MotionEvent.ACTION_UP:
                  /* Release the drag */
                  mIsBeingDragged = false;
                  mActivePointerId = INVALID_POINTER;
                  recycleVelocityTracker();
                  if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                      ViewCompat.postInvalidateOnAnimation(this);
                  }
                  // 停止嵌套滑动((前提是要作为NestedScrollingChild))
                  stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  break;
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  break;
          }
  ​
          /*
          * The only time we want to intercept motion events is if we are in the
          * drag mode.
          */
          return mIsBeingDragged;
      }

这个方法的代码量不大,部分说明已经写在注释里了,这里总结起来主要就是做了一下几件事情:

在ACTION_DOWN中:一个是判断是否需要拦截事件,二是在合适的时候调用startNestedScroll();方法

在ACTION_MOVE中:在需要处理的情况下,将mIsBeingDragged置为true,将事件传递给自己的onTouchEvent()方法进行处理。

在ACTION_UP或者ACTION_CANCEL中:将mIsBeingDragged重置为false,然后调用stopNestedScroll()停止嵌套滑动。

这里我们看下startNestedScroll();stopNestedScroll()的实现。

@Override
      public boolean startNestedScroll(int axes, int type) {
          return mChildHelper.startNestedScroll(axes, type);
      }
  ​
  @Override
      public void stopNestedScroll() {
          mChildHelper.stopNestedScroll();
      }

可以看到,就是简单代理给了ChildHelper进行处理,根据上篇文章的解析,我们知道这两个方法的调用,最终会进入到Parent对应的onStartNestedScroll(View child, View target, int nestedScrollAxes)onStopNestedScroll(View target)方法。那我们就继续看下这两个方法的处理吧

@Override
  public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
      // 只处理垂直滑动的情况
      return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
  }
  ​
  ​
  @Override
  public void onStopNestedScroll(View target) {
      mParentHelper.onStopNestedScroll(target);
      stopNestedScroll();
  }
  onStartNestedScroll()方法中,就是判断是否是垂直滑动,是的话,返回true,表示进行处理。

onStopNestedScroll(View target)方法,代理给ParentHelper,然后继续调用stopNestedScroll();继续通知它的NestedScrollingParent(如果有的情况)停止嵌套滑动。

PS:这里有个主意的地方哦,有些人看到这里可能会觉得这不是形成死循环了么stopNestedScroll()->mChildHelper.stopNestedScroll()->onStopNestedScroll()->stopNestedScroll(),形成一个闭环了。

有这个误解的朋友是因为没有区分NestedScrollingParent和NestedSrollingChild身份。当我们调用stopNestedScroll()方法的时候,当前的NestedScrollView是必须是具有Child的有效的身份,如果是Parent这个方法没有意义的,相当于是一个空方法。

对应的onStopNestedScroll(View target)方法,是作为Parent有效身份的时候,才会被回调。对于Child身份也是没有意义的。所以其实从stopNestedScroll()stopNestedScroll()已经从一个对象(Child)到另一个对象了(Parent)的传递,不是在同一个对象里的调用,所以是不会死循环的。

后面不会再做说明,记得所有重写的NestedScrollingChild接口的方法,只有在Child身份的对象上有效,所有重写的NestedScrollingParent接口的方法,只有在Parent身份的对象上有效。

到这里,startNestedScroll和stopNestedScroll这两组流程分析就完了。那中间真正的分发流程在哪儿呢?那就是onTouchEvent()方法啊,在上面onInterceptTouchEvent(MotionEvent ev)方法里面,适当的时候,不是做了拦截操作么,那就会进入onTouchEvent()方法咯,而且如果该类的子View没有消费掉触摸事件,正常情况也会再分发到该类的onTouchEvent()方法。

这里我们就继续这个方法的分析吧

@Override
      public boolean onTouchEvent(MotionEvent ev) {
          initVelocityTrackerIfNotExists();
  ​
          MotionEvent vtev = MotionEvent.obtain(ev);
  ​
          final int actionMasked = ev.getActionMasked();
  ​
          if (actionMasked == MotionEvent.ACTION_DOWN) {
              mNestedYOffset = 0;
          }
          vtev.offsetLocation(0, mNestedYOffset);
  ​
          switch (actionMasked) {
              case MotionEvent.ACTION_DOWN: {
                  if (getChildCount() == 0) {
                      // 没有Child,那还滑动个啥啊,都撑不开布局
                      return false;
                  }
                  if ((mIsBeingDragged = !mScroller.isFinished())) {
                      final ViewParent parent = getParent();
                      // 通知父View不要拦截触摸事件
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                  }
  ​
                  /*
                   * If being flinged and user touches, stop the fling. isFinished
                   * will be false if being flinged.
                   */
                  // 如果是在惯性滑动中 停止滑动
                  if (!mScroller.isFinished()) {
                      mScroller.abortAnimation();
                  }
  ​
                  // Remember where the motion event started
                  // 记录按下的位置
                  mLastMotionY = (int) ev.getY();
                  mActivePointerId = ev.getPointerId(0);
                  // 也是开启嵌套滑动
                  startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                  break;
              }
              case MotionEvent.ACTION_MOVE:
                  final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                  if (activePointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                      break;
                  }
  ​
                  final int y = (int) ev.getY(activePointerIndex);
                  // 手指滑动距离
                  int deltaY = mLastMotionY - y;
                  // 先分发给Parent进行预处理
                  if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                          ViewCompat.TYPE_TOUCH)) {
                      // 如果Parent消费了滑动距离 需要减去
                      deltaY -= mScrollConsumed[1];
                      vtev.offsetLocation(0, mScrollOffset[1]);
                      mNestedYOffset += mScrollOffset[1];
                  }
                  if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                      final ViewParent parent = getParent();
                      // 也是告知父View不要拦截事件
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                      mIsBeingDragged = true;
                      if (deltaY > 0) {
                          deltaY -= mTouchSlop;
                      } else {
                          deltaY += mTouchSlop;
                      }
                  }
                  if (mIsBeingDragged) {
                      // Scroll to follow the motion event
                      mLastMotionY = y - mScrollOffset[1];
                  
                      final int oldY = getScrollY();
                      final int range = getScrollRange();
                      final int overscrollMode = getOverScrollMode();
                      boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                              || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
  ​
                      // Calling overScrollByCompat will call onOverScrolled, which
                      // calls onScrollChanged if applicable.
                      // 滚动自己
                      if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                              0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                          // Break our velocity if we hit a scroll barrier.
                          mVelocityTracker.clear();
                      }
  ​
                      // 重新计算未消费的距离
                      final int scrolledDeltaY = getScrollY() - oldY;
                      final int unconsumedY = deltaY - scrolledDeltaY;
                      // 分发给Parent进行嵌套滚动
                      if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                              ViewCompat.TYPE_TOUCH)) {
                          mLastMotionY -= mScrollOffset[1];
                          vtev.offsetLocation(0, mScrollOffset[1]);
                          mNestedYOffset += mScrollOffset[1];
                      } else if (canOverscroll) {
                          // 如果Parent没有消费 并且可以滚动 继续处理
                          ensureGlows();
                          final int pulledToY = oldY + deltaY;
                          if (pulledToY < 0) {
                              EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
                                      ev.getX(activePointerIndex) / getWidth());
                              if (!mEdgeGlowBottom.isFinished()) {
                                  mEdgeGlowBottom.onRelease();
                              }
                          } else if (pulledToY > range) {
                              EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
                                      1.f - ev.getX(activePointerIndex)
                                              / getWidth());
                              if (!mEdgeGlowTop.isFinished()) {
                                  mEdgeGlowTop.onRelease();
                              }
                          }
                          if (mEdgeGlowTop != null
                                  && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                              ViewCompat.postInvalidateOnAnimation(this);
                          }
                      }
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  final VelocityTracker velocityTracker = mVelocityTracker;
                  velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                  int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                  if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                      // 如果达到了惯性的速度 分发惯性滑动事件
                      flingWithNestedDispatch(-initialVelocity);
                  } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                          getScrollRange())) {
                      ViewCompat.postInvalidateOnAnimation(this);
                  }
                  mActivePointerId = INVALID_POINTER;
                  // 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  endDrag();
                  break;
              case MotionEvent.ACTION_CANCEL:
                  if (mIsBeingDragged && getChildCount() > 0) {
                      if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                              getScrollRange())) {
                          ViewCompat.postInvalidateOnAnimation(this);
                      }
                  }
                  mActivePointerId = INVALID_POINTER;
                  // 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  endDrag();
                  break;
              case MotionEvent.ACTION_POINTER_DOWN: {
                  final int index = ev.getActionIndex();
                  mLastMotionY = (int) ev.getY(index);
                  mActivePointerId = ev.getPointerId(index);
                  break;
              }
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                  break;
          }
  ​
          if (mVelocityTracker != null) {
              mVelocityTracker.addMovement(vtev);
          }
          vtev.recycle();
          // 这里始终返回true 所以Parent作为父View其实是进不了onTouchEvent()方法的。
          return true;
      }

这个方法比较长,里面涉及到一些具体滚动操作,这不是我们本篇的重点,所以我们看关键地方就可以了。重要地方我都写了注释说明。

先看返回值,这里直接返回了true,从这里知道,Parent和Child嵌套的时候,Parent是肯定进不了该方法的。所以这里面的情况,我们只需要考虑Child身份即可。

ACTION_DOWN中

如果是惯性滑动的情况,停止滑动。同样是调用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);开启嵌套滑动,所以Parent中的onStartNestedScroll()可能不止一次调用。但是多次调用有影响吗?没影响,在里面没有做具体滚动操作,只是做是否需要处理的判断而已。

ACTION_MOVE中

先通过调用dispatchNestedPreScroll()分发给Parent进行滚动处理。然后再通过overScrollByCompat()自己处理滚动事件,最后再计算一下未消费的距离,再通过dispatchNestedScroll()继续给Parent进行处理。同时根据返回值,判断Parent是否处理了,进行下一步操作。

这里的dispatchNestedPreScroll(),就会进入Parent的onNestedPreScroll()的方法,我们看下处理:

@Override
  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
      dispatchNestedPreScroll(dx, dy, consumed, null);
  }

可以看到其实啥也没做,就是继续给它自己的Parent(如果有的情况)分发事件

接下来再看下dispatchNestedScroll()对应的Parent的onNestedScroll()方法

@Override
      public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
              int dyUnconsumed) {
          final int oldScrollY = getScrollY();
          // 滚动自己 消费掉距离
          scrollBy(0, dyUnconsumed);
          final int myConsumed = getScrollY() - oldScrollY;
          final int myUnconsumed = dyUnconsumed - myConsumed;
          // 继续分发给上一级
          dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
      }

这里面也比较简单,就是使用scrollBy()方法,滚动自己。消费掉滚动距离。同样在自己还有Parent的情况下,继续向上分发。

到这里,ACTION_DOWN的情况,我们就介绍完了,继续看UP和CANCEL

ACTION_UP和ACTION_CANCEL中:

在这两个case中,最后都调用了endDrag()。我们看下这个方法

private void endDrag() {
          mIsBeingDragged = false;
  ​
          recycleVelocityTracker();
          // 停止嵌套滚动 
          stopNestedScroll(ViewCompat.TYPE_TOUCH);
  ​
          if (mEdgeGlowTop != null) {
              mEdgeGlowTop.onRelease();
              mEdgeGlowBottom.onRelease();
          }
      }

这里面就是调用stopNestedScroll(ViewCompat.TYPE_TOUCH);,这个方法前面已经分析了,这里不做多的说明。

回到ACTION_UP中。这里面在调用endDrag()之前,还调用了flingWithNestedDispatch()方法,看下具体实现:

 private void flingWithNestedDispatch(int velocityY) {
          final int scrollY = getScrollY();
          final boolean canFling = (scrollY > 0 || velocityY > 0)
                  && (scrollY < getScrollRange() || velocityY < 0);
          // 先给Parent看是否需要处理
          if (!dispatchNestedPreFling(0, velocityY)) {
              // 再次回调Parent,其实主要目的通过canFling参数,是告诉Parent我自己处理了
              dispatchNestedFling(0, velocityY, canFling);
              // 没有处理 自己处理
              fling(velocityY);
          }
      }
 这里面就是继续惯性滑动事件的分发而已,注释说的很清楚了,就不解释了。那我们就继续看Parent中对应的两个方法:onNestedPreScroll()和onNestedFling()
@Override
      public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
          if (!consumed) {
              flingWithNestedDispatch((int) velocityY);
              return true;
          }
          return false;
      }
  ​
      @Override
      public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
          return dispatchNestedPreFling(velocityX, velocityY);
      }

处理很简单,onNestedPreFling()直接往上一级Parent分发,onNestedFling()直接,看Child是否消费了,没有消费往上一级Parent分发。并返回true,如果已经消费了,直接返回fasle即可。

到这里,整个流程就分析完了,还是做一个简单的总结吧。

3、总结

大家再通过这张流程图(画的不好,将就看了)自己回忆和梳理一下吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值