Android中的触摸事件——嵌套滚动

Android中的触摸事件——嵌套滚动

前言

相信绝大部分的Android开发者都遇到过嵌套滑动冲突的问题,后来Google官方也推出了许多解决嵌套滑动冲突的控件。最经典的就是NestedScrollView和RecyclerView。特别是NestedScrollView,他几乎完全取代了ScrollView。Google的这些嵌套滑动控件绝大部分都是通过实现NestedScrollingChild3NestedScrollingParent3这两个接口来实现的。当我明白了他们对于嵌套滑动是如何实现的时候,脑袋中就直接冒出两个字——“妙啊”。下面的内容也是以分析NestedScrollView的代码来分析嵌套滑动的处理。

正文

NestedScrollingChild3和NestedScorllingParent3

就像它们的命名一样,NestedScrollingChild3就是那些需要能够滑动同时希望能够支持嵌套滑动的View实现的接口;NestedScrollingParent3就是能够处理Child中有嵌套滑动View的情况。可能描述有点抽象。这里举个例子:假如一个NestedScrollView中有一个RecyclerView,当RecyclerView滑动到底部的时候,再向上滑动,这个时候NestedScrollView就会滑动。在这个例子中RecyclerView就是Child(NestedScrollingChild3的简称,下文也一样),NestedScrollView就是Parent(NestedScrollingParent3的简称,下文也一样)。当Child滑动到底部的时候,再向上滑动就会通知Parent自己还在滑动但是没有办法消费这个滑动距离,然后让Parent来处理这个未消费的滑动距离,当然Parent也可以不处理,可以找它的Parent来处理,依次类推,可以一直向上找处理这个滑动距离的View。所以一个View即可以是Child也可以是Parent,NestedScrollView就同时实现了这两个接口。

  1. NestedScrollingChild3#startNestedScroll

这个方法是滑动开始的时候会调用的方法,一般是ACTION_DOWN的时候会调用这个方法,默认情况下,这个方法回去找实现了Parent接口的父View,如果找到了这个View然后就会调用Parent的onStartNestedScroll方法,如果这个方法返回true,表示父View接受了Child的嵌套滑动,Child这时就会把这个Parent保存下来,同时再调用Parent的onNestedScrollAccepted方法,这个方法中会吧Child的滚动方向记录下来。总结一下就是:Child#startNestedScroll -> Parent#onStartNestedScroll -> Parent#onNestedScrollAccepted

  1. NestedScrollingChild3#dispatchNestedPreScroll

在Child准备滑动之前会调用这个方法,然后再调用startNestedScroll过程中找到的Parent的onNestedPreScroll方法。这个过程做一个简单的比喻:就好比你想要买一个10000块的电脑,然后你要向老板申请,老板可以拒绝你的申请,也可以只让你买8000的,可能同意你买10000的。在这里你就是Child而老板就是Parent,10000就是Child想要滑动的距离。总结:Child#dispatchNestedPreScroll -> Parent#onNestedPreScroll

  1. NestedScrollingChild3#dispatchNestedScroll

当Child滑动完成后,会调用这个方法,然后调用Parent的onNestedScroll方法。这个过程中有可能Child自己并没有消费完这次的滑动距离,然后把这个没有消费完成的距离交给Parent处理,也就是在上面提到的例子。总结:Child#dispatchNestedScroll -> Parent#onNestedScroll

  1. NestedScrollingChild3#dispatchNestedPreFling

当快速滑动前会调用这个方法,这个方法和dispatchNestedPreScroll方法类似,不同的是:这个方法只会返回两种情况,一种是这次快速滑动被Parent处理,另一种是没有被Parent处理。总结:Child#dispatchNestedPreFling-> Parent#onNestedPreFling

  1. NestedScrollingChild3#dispatchNestedFling

当Child确认处理快速滑动后,调用这个方法。然后Parent能够收到Child的滑动的速度。总结:Child#dispatchNestedFling -> Parent#onNestedFling

  1. NestedScrollingChild3#stopNestedScroll

结束滑动时调用这个方法和startNestedScroll方法对应。这个方法会调用Parent中的onStopNestedScroll,然后再将parent清空。总结:Child#startNestedScroll -> Parent#onStopNestedScroll

Scroller简单介绍

有的人可能不熟悉Scroller,但是你肯定熟悉ValueAnimator。它们的实现都是大同小异,都是将用来计算一个值到另一个值的变化过程。不同是Scroller是用来计算滑动,ValueAnimator是用来计算动画。在很多的可以滑动的View中都可以见到它的身影。注意它本身并不实际参与View的滑动,View的滑动是View调用scrollToscrollBy这两个方法来滑动的,这两个方法相信大家都很熟悉。
这里以快速滑动这个场景举一个例子。快速滑动会调用Scroller的fling方法,这个方法中可以传入开始滑动的位置,滑动的速度,最大和最小滑动的位置。当调用fling方法后然后再调用postInvalidateOnAnimation方法,这个方法会申请View的重绘,在重绘的过程中会调用View的computeScroll方法,这个方法默认是空实现,需要我们重写,在这个方法中我们可以调用scroller的computeScrollOffset来完成这一帧的滑动位置的计算,然后调用View的scrollTo或者scrollBy来完成实际View滑动,这时如果scroller的计算还没有完成,就再次调用postInvalidateOnAnimation方法申请继续重新绘制View。在文章后面的NestedScrollView的代码分析中能够看到这部分的具体代码。

NestedScrollView嵌套滑动的实现

经过前面Scroller和嵌套滑动接口知识的铺垫,还有前面两篇系列文章(多点触控,触摸事件下发)的介绍,现在就可以开始对NestedScrollView源码进行分析了。源码分析可能会迟到,但永远不会缺席。

首先分析的是onInterceptTouchEvent方法,这个方法在前面的文章介绍过,用来控制是否拦截子View的触摸事件。



  @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();
      
      // 如果是MOVE事件同时已经被被拖拽住,直接拦截。
      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);
              
              // 当滑动的距离大于touchSlop同时子View嵌套滑动方向不是垂直方向时,该次事件流就会被
              // NestedScrollView接管。这里很重要,我举一个例子:当Child中有一个Button,用户用手按住Button
              // 这个时候NestedScrollView会把这次事件交给Button处理,但是在这个过程中你的手又向上滑动了,那么
              // 这次事件将被NSV拦截掉,然后不再给Button处理,NSV自己处理。
              if (yDiff > mTouchSlop
                      && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                  
                  // 确认被拖拽
                  mIsBeingDragged = true;
                  mLastMotionY = y;
                  
                  // 初始化velocityTracker,用来计算用户滑动的速度
                  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();
              // 如果点击不再Child内,停止拖拽。
              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.
              */
              
              // 判断scroller的滑动是否完成,如果没有完成则标记为拖拽。
              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();
              
              // 滑动到边界的时候,实现弹簧效果(NestedScrollView中没有边界的弹簧效果,后面这个代
              // 码中我就不分析它了),这段代码现目前应该是没有用的。
              if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                  ViewCompat.postInvalidateOnAnimation(this);
              }
              // 结束嵌套滑动
              stopNestedScroll(ViewCompat.TYPE_TOUCH);
              break;
          case MotionEvent.ACTION_POINTER_UP:
                 
                 // 多点触控的处理,在前面的文章中有分析,后面在onTouchEvent中再简单分析下。
              onSecondaryPointerUp(ev);
              break;
      }

      /*
      * The only time we want to intercept motion events is if we are in the
      * drag mode.
      */
      // 如果已经被拖拽,就拦截。
      return mIsBeingDragged;
  }


上面代码中主要是判断拖拽,如果已经拖拽就拦截。这里简单总结下拦截的情况:1. DOWN:当点击的位置不在Child内时,停止拦截。2. DOWN:如果当前滑动未结束,拦截。3. MOVE: 如果Child没有在垂直方向上的滑动,则拦截滑动。

下面再分析下startNestedScrollstopNestedScroll中的代码:


  
   // 直接调用的Helper中的startNestedScroll方法,这个helper是NestedScrollingChildHelper,还有
   // parentHelper,类型是NestedScrollingParentHelper。在初始化这两个helper的时候记得开启嵌套滑动。  
  @Override
  public boolean startNestedScroll(int axes, int type) {
      return mChildHelper.startNestedScroll(axes, type);
  }


  // NestedScrollingChildHelper#startNestedScroll
  /**
   * Start a new nested scroll for this view.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
   * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
   * signature to implement the standard policy.</p>
   *
   * @param axes Supported nested scroll axes.
   *             See {@link androidx.core.view.NestedScrollingChild2#startNestedScroll(int,
   *             int)}.
   * @return true if a cooperating parent view was found and nested scrolling started successfully
   */
  public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
      // axes: 滑动的方向,水平和垂直。  
      // type:这里的type一共有两种,TOUCH和NONE TOUCH。分别对应通过Touch滑动和非Touch滑动(例如惯性滑动),
      // 后面很多方法中都有这个参数就不赘述了。
      
      // 如果已经有parent处理了就直接返回。
      if (hasNestedScrollingParent(type)) {
          // Already in progress
          return true;
      }
      
      // 支持嵌套滑动时
      if (isNestedScrollingEnabled()) {
          ViewParent p = mView.getParent();
          View child = mView;
          // 遍历child的parent
          while (p != null) {
                  // 调用Parent的onStartNestedScroll方法,当返回为true时表示,这个parent接收了这次嵌套滑动。 
              if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                      
              
                      // 这个方法会将这个parent和对应的type保存下来,方便后面的一系列操作。  
                  setNestedScrollingParentForType(type, p);
                  
                  // 调用parent的Accepted方法。
                  ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                  return true;
              }
              if (p instanceof View) {
                  child = (View) p;
              }
              p = p.getParent();
          }
      }
      return false;
  }
  
  // NestedScrollView#onStartNestedScroll方法
  // 判断滑动方向是不是水平的,如果是就接受这次嵌套滑动。  
  @Override
  public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
          int type) {
      return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
  }
  
  // NestedScrollView#onNestedScrollAccepted
  
  // 这里会调用parentHelper的onNestedScrollAccepted方法,然后又调用了startNestedScroll方法(也就是再找
  // 它的parent来处理这次嵌套滑动)。
  @Override
  public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
          int type) {
      mParentHelper.onNestedScrollAccepted(child, target, axes, type);
      startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
  }
  
  // NestedScrollingParentHelper#onNestedScrollAccepted
  
  /**
   * Called when a nested scrolling operation initiated by a descendant view is accepted
   * by this ViewGroup.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup}
   * subclass method/{@link androidx.core.view.NestedScrollingParent2} interface method with
   * the same signature to implement the standard policy.</p>
   */
  // 将接受的子View的滑动type保存下来。  
  public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
          @ScrollAxis int axes, @NestedScrollType int type) {
      if (type == ViewCompat.TYPE_NON_TOUCH) {
          mNestedScrollAxesNonTouch = axes;
      } else {
          mNestedScrollAxesTouch = axes;
      }
  }

这里简单总结下start的过程,Child调用startNestedScroll,这里会寻找一个Parent来处理这次嵌套滑动,Parent会通过调用onStartNestedScroll方法来确认是否(是否是垂直方向)接受这次滑动,如果接受这次滑动,Parent还会调用onNestedScrollAccepted方法同时又调用了自己的startNestedScroll方法(这时的Parent又作为一个Child去寻找它的parent来处理这次嵌套滑动,通俗点来说就是“套娃”,有很多这样的“套娃”操作,后面就不详说明了)。


  
  // NestedScrollView#stopNestedScroll
  @Override
  public void stopNestedScroll(int type) {
      mChildHelper.stopNestedScroll(type);
  }
  
  //NestedScrollingChildHelper#stopNestedScroll
  /**
   * Stop a nested scroll in progress.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
   * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
   * signature to implement the standard policy.</p>
   */
  public void stopNestedScroll(@NestedScrollType int type) {
      ViewParent parent = getNestedScrollingParentForType(type);
      if (parent != null) {
          // 调用Parent的onStopNestedScroll方法
          ViewParentCompat.onStopNestedScroll(parent, mView, type);
          // 清除在start过程中的parent
          setNestedScrollingParentForType(type, null);
      }
  }
  
  // NestedScrollView#onStopNestedScroll
  @Override
  public void onStopNestedScroll(@NonNull View target, int type) {
      
      mParentHelper.onStopNestedScroll(target, type);
      // 又是套娃操作,不多说了
      stopNestedScroll(type);
  }
  
  // NestedScrollingParentHelper
  /**
   * React to a nested scroll operation ending.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup}
   * subclass method/{@link androidx.core.view.NestedScrollingParent2} interface method with
   * the same signature to implement the standard policy.</p>
   */
  public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
      // 清除在accept过程中保存下来的Child的滑动方向。
      if (type == ViewCompat.TYPE_NON_TOUCH) {
          mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
      } else {
          mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
      }
  }

上面的stop过程很简单,这里不多说了。

下面分析重要的onTouchEvent方法,滑动的处理主要逻辑都是在这里完成的。



  @Override
  public boolean onTouchEvent(MotionEvent ev) {
      
      // 如果velocityTracker不存在,添加一个对象(用于惯性滑动的速度计算)。
      initVelocityTrackerIfNotExists();

      final int actionMasked = ev.getActionMasked();

      if (actionMasked == MotionEvent.ACTION_DOWN) {
          mNestedYOffset = 0;
      }
      
       // clone一个MotionEvent用于速度计算
      MotionEvent vtev = MotionEvent.obtain(ev);
      vtev.offsetLocation(0, mNestedYOffset);

      switch (actionMasked) {
          case MotionEvent.ACTION_DOWN: {
              if (getChildCount() == 0) {
                  return false;
              }
              
              // 如果滑动没有完成会先禁止parent对事件的拦截
              if ((mIsBeingDragged = !mScroller.isFinished())) {
                  final ViewParent parent = getParent();
                  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()) {
                  abortAnimatedScroll();
              }

              // 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;
              
              // 如果没有被拖拽,同时滑动距离大于touchSlop,就会把状态设置为拖拽,禁止parent拦截事件
              if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                  final ViewParent parent = getParent();
                  if (parent != null) {
                      parent.requestDisallowInterceptTouchEvent(true);
                  }
                  mIsBeingDragged = true;
                  if (deltaY > 0) {
                      deltaY -= mTouchSlop;
                  } else {
                      deltaY += mTouchSlop;
                  }
              }
              
              // 如果已经拖拽
              if (mIsBeingDragged) {
                  // Start with nested pre scrolling
                  
                  // 调用dispatchNestedPreScroll方法
                  // delatY:想要滑动的垂直距离。
                  // scrollConsumed:被Parent消费的距离
                  // scrollOffset:当前View的位置位移的距离(这里举个例子:很多人都用过可以折叠的Toolbar吧
                  // ,那种情况下,在滑动的时候就会改变当前View的位置)
                  if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                          ViewCompat.TYPE_TOUCH)) {
                      // 减去parent消费的距离
                      deltaY -= mScrollConsumed[1];
                      // View的位移距离
                      mNestedYOffset += mScrollOffset[1];
                  }

                  // Scroll to follow the motion event
                  mLastMotionY = y - mScrollOffset[1];
                      
                      // 记录下当前的滑动位置,用于后面的实际滑动后的消费距离计算。
                  final int oldY = getScrollY();
                  // 在Y轴上可以滑动的最大距离,最小为0
                  final int range = getScrollRange();
                  
                  // 滑动到边界的处理模式,相信很多人在XML的属性里面见过。
                  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.
                  // overScrollByCompat这个方法里面处理实际滑动,当滑动到边界时会返回true,这时会把
                  // velocityTracker清空。
                  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;

                  mScrollConsumed[1] = 0;

                      // 把没有消费的距离交给Parent,就是我们上面提到过的嵌套滑动的例子。 
                  dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                          ViewCompat.TYPE_TOUCH, mScrollConsumed);

                  mLastMotionY -= mScrollOffset[1];
                  mNestedYOffset += mScrollOffset[1];
                      
                      // 这下面的代码就是处理滑动到边界时的气泡一样的UI,主要用到了EdgeEffect这个类完成计算和
                      // 绘制,在draw方法里面有具体的绘制代码。
                  if (canOverscroll) {
                      deltaY -= mScrollConsumed[1];
                      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);
              
              // 当滑动的速度大于minVelocity时
              if ((Math.abs(initialVelocity) >= mMinimumVelocity)) {
                  // 如果这次惯性滑动没有被Parent消费,那么自己消费
                  if (!dispatchNestedPreFling(0, -initialVelocity)) {
                      dispatchNestedFling(0, -initialVelocity, true);
                      
                      // 处理惯性滚动的方法
                      fling(-initialVelocity);
                  }
              } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                      getScrollRange())) {
                  ViewCompat.postInvalidateOnAnimation(this);
              }
              mActivePointerId = INVALID_POINTER;
              
              // 结束滑动,并初始化一些状态
              endDrag();
              break;
          case MotionEvent.ACTION_CANCEL:
              // 和ACTION_UP相比,去掉了惯性滑动的代码
              if (mIsBeingDragged && getChildCount() > 0) {
                  if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                          getScrollRange())) {
                      ViewCompat.postInvalidateOnAnimation(this);
                  }
              }
              mActivePointerId = INVALID_POINTER;
              endDrag();
              break;
          case MotionEvent.ACTION_POINTER_DOWN: {
              // 多点触控的处理,和我之前分析RecyclerView中的多点触控的处理方式一样。
              // 当有新的触摸点添加到屏幕上的时候,新添加的触摸点接管滑动。 
              final int index = ev.getActionIndex();
              mLastMotionY = (int) ev.getY(index);
              mActivePointerId = ev.getPointerId(index);
              break;
          }
          case MotionEvent.ACTION_POINTER_UP:
              // 多点触控处理
              // 当有触摸点离开时:选择index为0的触摸点,如果离开的是index为0的触摸点,就选择
              // index为1的。
              onSecondaryPointerUp(ev);
              mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
              break;
      }
       
       // 将event添加到velocityTracker用户速度计算
      if (mVelocityTracker != null) {
          mVelocityTracker.addMovement(vtev);
      }
      vtev.recycle();

      return true;
  }


  // UP 和 CANCEL 的时候重置一些状态。
  private void endDrag() {
      mIsBeingDragged = false;

      recycleVelocityTracker();
      stopNestedScroll(ViewCompat.TYPE_TOUCH);

      if (mEdgeGlowTop != null) {
          mEdgeGlowTop.onRelease();
          mEdgeGlowBottom.onRelease();
      }
  }


上面就是滑动的主要代码和逻辑,这里简单总结一下:1. 首先在ACTION_DOWN的时候:如果scroller还没有完成,禁止parent的拦截,停止scroller没有完成的滑动,开始一次新的滑动。 2. ACTION_MOVE:如果确定滑动:禁止parent拦截事件;调用dispatchPreNestedScroll方法,如果parent有消费距离,dy还会再减去parent消费的距离,进行实际的滑动,实际滑动后还会调用dispatchNestedScroll方法将本次滑动的距离和没有消费的距离传个parent, 如果滑动到边界,再处理滑动的气泡UI。3. ACTION_UP: 计算本次滑动事件的速度,然后交给scroller处理这次快速滑动,同时重置一些状态。

下面贴出dispatchNestedPreScroll、dispatchNestedScroll、dispatchNestedPreFling和dispatchNestedFling代码。


   // NestedScrollView#dispatchNestedPreScroll
  @Override
  public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
          int type) {
      return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
  }
  
  // NestedScrollingChildHelper#dispatchNestedPreScroll
  /**
   * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
   * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
   * signature to implement the standard policy.</p>
   *
   * @return true if the parent consumed any of the nested scroll
   */
  public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
          @Nullable int[] offsetInWindow, @NestedScrollType int type) {
      if (isNestedScrollingEnabled()) {
          final ViewParent parent = getNestedScrollingParentForType(type);
          if (parent == null) {
              return false;
          }

          if (dx != 0 || dy != 0) {
              int startX = 0;
              int startY = 0;
              if (offsetInWindow != null) {
                  mView.getLocationInWindow(offsetInWindow);
                  startX = offsetInWindow[0];
                  startY = offsetInWindow[1];
              }

              if (consumed == null) {
                  consumed = getTempNestedScrollConsumed();
              }
              consumed[0] = 0;
              consumed[1] = 0;
              ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

              if (offsetInWindow != null) {
                  mView.getLocationInWindow(offsetInWindow);
                  offsetInWindow[0] -= startX;
                  offsetInWindow[1] -= startY;
              }
              return consumed[0] != 0 || consumed[1] != 0;
          } else if (offsetInWindow != null) {
              offsetInWindow[0] = 0;
              offsetInWindow[1] = 0;
          }
      }
      return false;
  }
  
  // NestedScrollView#onNestedPreScroll
  
  @Override
  public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
          int type) {
      // 这里又是套娃操作。
      dispatchNestedPreScroll(dx, dy, consumed, null, type);
  }
  

dispatchNestedPreScroll逻辑很简单,就不多说了,最后也有套娃操作。


  
   // NestedScrollView#dispatchNestedScroll
  @Override
  public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
          int dyUnconsumed, int[] offsetInWindow) {
      return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
              offsetInWindow);
  }
  
  // NestedScrollingChildHelper#dispatchNestedScroll
  /**
   * Dispatch one step of a nested scrolling operation to the current nested scrolling parent.
   *
   * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
   * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same
   * signature to implement the standard policy.</p>
   *
   * @return <code>true</code> if the parent consumed any of the nested scroll distance
   */
  public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
          int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
      return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
              offsetInWindow, TYPE_TOUCH, null);
  }
  
  
  // NestedScrollingChildHelper#dispatchNestedScrollInternal
  private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
          int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
          @NestedScrollType int type, @Nullable int[] consumed) {
      if (isNestedScrollingEnabled()) {
          final ViewParent parent = getNestedScrollingParentForType(type);
          if (parent == null) {
              return false;
          }

          if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
              int startX = 0;
              int startY = 0;
              if (offsetInWindow != null) {
                  mView.getLocationInWindow(offsetInWindow);
                  startX = offsetInWindow[0];
                  startY = offsetInWindow[1];
              }

              if (consumed == null) {
                  consumed = getTempNestedScrollConsumed();
                  consumed[0] = 0;
                  consumed[1] = 0;
              }

              ViewParentCompat.onNestedScroll(parent, mView,
                      dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

              if (offsetInWindow != null) {
                  mView.getLocationInWindow(offsetInWindow);
                  offsetInWindow[0] -= startX;
                  offsetInWindow[1] -= startY;
              }
              return true;
          } else if (offsetInWindow != null) {
              // No motion, no dispatch. Keep offsetInWindow up to date.
              offsetInWindow[0] = 0;
              offsetInWindow[1] = 0;
          }
      }
      return false;
  }
  
  // NestedScrollView#onNestedScroll
  
  @Override
  public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
          int dxUnconsumed, int dyUnconsumed) {
      onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null);
  }
  
  // 这里需要注意了,这里不仅仅是进行了套娃操作
  private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
      final int oldScrollY = getScrollY();
      // 消费Child没有消费的距离
      scrollBy(0, dyUnconsumed);
      final int myConsumed = getScrollY() - oldScrollY;

      if (consumed != null) {
          consumed[1] += myConsumed;
      }
      final int myUnconsumed = dyUnconsumed - myConsumed;
      
       // 套娃操作
      mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
  }

在NestedScrollView的onNestedScroll方法中,如果对Child没有消费的距离进行了处理。

到这里在NestedScrollView中的嵌套滑动处理的代码都分析完了,Child中调用startXXXXX, stopXXXXX, dispatchXXXX等方法,最后这些方法会调用接受处理的Parent中的onXXXX方法,如果这个Parent也是作为一个Child,还会继续套娃操作,继续调用上面Child对应的方法。这里补充说明下,在CoordinatorLayout中它又会把Parent中的onXXXX方法全部交给Behavior处理,Behavior要处理的事情实在有点多,它可能会处理滑动过程,可能会处理Layout过程。如果有人想要分析Behavior,这是一个重要的突破口。

下面的代码简单分析下NestedScrollView中真实滑动相关的代码。


   
   // 下面是绘制气泡的代码算法,主要都是EdgeEffect这个对象处理了,还是很简单的。不多描述了。
  @Override
  public void draw(Canvas canvas) {
      super.draw(canvas);
      if (mEdgeGlowTop != null) {
          final int scrollY = getScrollY();
          if (!mEdgeGlowTop.isFinished()) {
              final int restoreCount = canvas.save();
              int width = getWidth();
              int height = getHeight();
              int xTranslation = 0;
              int yTranslation = Math.min(0, scrollY);
              if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
                  width -= getPaddingLeft() + getPaddingRight();
                  xTranslation += getPaddingLeft();
              }
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
                  height -= getPaddingTop() + getPaddingBottom();
                  yTranslation += getPaddingTop();
              }
              canvas.translate(xTranslation, yTranslation);
              mEdgeGlowTop.setSize(width, height);
              if (mEdgeGlowTop.draw(canvas)) {
                  ViewCompat.postInvalidateOnAnimation(this);
              }
              canvas.restoreToCount(restoreCount);
          }
          if (!mEdgeGlowBottom.isFinished()) {
              final int restoreCount = canvas.save();
              int width = getWidth();
              int height = getHeight();
              int xTranslation = 0;
              int yTranslation = Math.max(getScrollRange(), scrollY) + height;
              if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
                  width -= getPaddingLeft() + getPaddingRight();
                  xTranslation += getPaddingLeft();
              }
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
                  height -= getPaddingTop() + getPaddingBottom();
                  yTranslation -= getPaddingBottom();
              }
              canvas.translate(xTranslation - width, yTranslation);
              canvas.rotate(180, width, 0);
              mEdgeGlowBottom.setSize(width, height);
              if (mEdgeGlowBottom.draw(canvas)) {
                  ViewCompat.postInvalidateOnAnimation(this);
              }
              canvas.restoreToCount(restoreCount);
          }
      }
  }


上面绘制气泡的算法还是不难,这里不详细分析了。


  // 这个方法中还支持水平滑动,然而实际的代码中并没有水平滑动的代码。
  boolean overScrollByCompat(int deltaX, int deltaY,
          int scrollX, int scrollY,
          int scrollRangeX, int scrollRangeY,
          int maxOverScrollX, int maxOverScrollY,
          boolean isTouchEvent) {
      final int overScrollMode = getOverScrollMode();
      final boolean canScrollHorizontal =
              computeHorizontalScrollRange() > computeHorizontalScrollExtent();
      final boolean canScrollVertical =
              computeVerticalScrollRange() > computeVerticalScrollExtent();
      final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS
              || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
      final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS
              || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
          
      int newScrollX = scrollX + deltaX;
      if (!overScrollHorizontal) {
          maxOverScrollX = 0;
      }
       
       // 希望滑动到的最终位置
      int newScrollY = scrollY + deltaY;
      // 当不支持OverScroll的时候最大的OverScroll值为0
      if (!overScrollVertical) {
          maxOverScrollY = 0;
      }

      // Clamp values if at the limits and record
      final int left = -maxOverScrollX;
      final int right = maxOverScrollX + scrollRangeX;
      final int top = -maxOverScrollY;
      final int bottom = maxOverScrollY + scrollRangeY;

      boolean clampedX = false;
      if (newScrollX > right) {
          newScrollX = right;
          clampedX = true;
      } else if (newScrollX < left) {
          newScrollX = left;
          clampedX = true;
      }

      boolean clampedY = false;
      //当滑动的最终的位置大于child的bottom时,最终滑动位置设置为bottom
      if (newScrollY > bottom) {
          newScrollY = bottom;
          clampedY = true;
      } else if (newScrollY < top) {
            // 当最终位置小于child的top时,将滑动最终位置设置为top。
          newScrollY = top;
          clampedY = true;
      }

      if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
          mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange());
      }

      onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

      return clampedX || clampedY;
  }
  
  // 直接调用scrollTo方法完成滑动
  @Override
  protected void onOverScrolled(int scrollX, int scrollY,
          boolean clampedX, boolean clampedY) {
      super.scrollTo(scrollX, scrollY);
  }


当调用overScrollByCompat返回为true时,表示已经滑到边界。



  /**
   * Fling the scroll view
   *
   * @param velocityY The initial velocity in the Y direction. Positive
   *                  numbers mean that the finger/cursor is moving down the screen,
   *                  which means we want to scroll towards the top.
   */
  public void fling(int velocityY) {
      if (getChildCount() > 0) {

          mScroller.fling(getScrollX(), getScrollY(), // start
                  0, velocityY, // velocities
                  0, 0, // x
                  Integer.MIN_VALUE, Integer.MAX_VALUE, // y
                  0, 0); // overscroll
          runAnimatedScroll(true);
      }
  }
  
  
  private void runAnimatedScroll(boolean participateInNestedScrolling) {
      if (participateInNestedScrolling) {
            // 开始滑动
          startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
      } else {
          stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
      }
      // 记录上次滑动的位置
      mLastScrollerY = getScrollY();
      // 请求View重新绘制
      ViewCompat.postInvalidateOnAnimation(this);
  }
  
  // 当重新绘制时会调用这个方法来请求scroll位置的计算。
  @Override
  public void computeScroll() {
      
       // 如果滑动已经完成,直接返回。 
      if (mScroller.isFinished()) {
          return;
      }

       // 计算滑动位置
      mScroller.computeScrollOffset();
      
      // 计算出来的y的位置
      final int y = mScroller.getCurrY();
      // dY
      int unconsumed = y - mLastScrollerY;
      mLastScrollerY = y;

      // Nested Scrolling Pre Pass
      mScrollConsumed[1] = 0;
      // 提前告诉Parent自己要滑动了,注意这里的type是NON_TOUCH
      dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,
              ViewCompat.TYPE_NON_TOUCH);
      
      // 减去被Parent消费的距离
      unconsumed -= mScrollConsumed[1];

      final int range = getScrollRange();

      if (unconsumed != 0) {
          // Internal Scroll
          final int oldScrollY = getScrollY();
          // 调用滑动代码。
          overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false);
          
          // 已经滑动的距离
          final int scrolledByMe = getScrollY() - oldScrollY;
          unconsumed -= scrolledByMe;

          // Nested Scrolling Post Pass
          mScrollConsumed[1] = 0;
          
          // 给Parent处理未滑动的距离。
          dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset,
                  ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
          unconsumed -= mScrollConsumed[1];
      }
       
       // 如果滑动到边界
      if (unconsumed != 0) {
          final int mode = getOverScrollMode();
          final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
                  || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
          
         
          if (canOverscroll) {
              ensureGlows();
              
              // 处理滑动气泡动画
              if (unconsumed < 0) {
                  if (mEdgeGlowTop.isFinished()) {
                      mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                  }
              } else {
                  if (mEdgeGlowBottom.isFinished()) {
                      mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                  }
              }
          }
          // 停止滑动
          abortAnimatedScroll();
      }
        
       // 如果嵌套滑动没有结束,请求View刷新,计算下一帧滑动位置。 
      if (!mScroller.isFinished()) {
          ViewCompat.postInvalidateOnAnimation(this);
      } else {
          // 结束这次嵌套滑动。
          stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
      }
  }
  
  private void abortAnimatedScroll() {
      mScroller.abortAnimation();
      stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
  }

在嵌套滑动中startNestedScroll和stopNestedScroll这两个方法中总是成对出现,又开始就有结束嘛。fling代码也还是比较简单,前面讲Scroller也有说过,scroller只是负责计算,真实的滑动也还是和onTouchEvent中一样,都是调用的overScrollByCompat方法。上面代码我相信对于你们也没有压力。

One More Thing, 由于NestedScrollView是继承于FrameLayout,默认在测量子View的时候,子View的最大高度是没有办法超过Parent的高度的,如果都没有办法超过Parent,那怎么滑动?所以必须重写measureChild方法



  @Override
  protected void measureChild(View child, int parentWidthMeasureSpec,
          int parentHeightMeasureSpec) {
      ViewGroup.LayoutParams lp = child.getLayoutParams();

      int childWidthMeasureSpec;
      int childHeightMeasureSpec;

      childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
              + getPaddingRight(), lp.width);

       // 不限制Child的高度。 
      childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }


Emmmmmm,到这里NestedScrollView的嵌套滚动的源码分析也就完成了,NestedScrollView什么都好,但是不支持水平方向上的嵌套滑动。所以我自己又写了一个支持水平方向上的嵌套滚动View--HorizontalNestedScrollView。算法基本和NestedScrollView差不多。

HorizontalNestedScrollView.gif









前面也提到了,Behavior也会影响嵌套滑动,如果有时间再做一篇关于Behavior相关的文章吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容