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、總結

大家再通過這張流程圖(畫的不好,將就看了)自己回憶和梳理一下吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章