NestedScrolling機制解析(一)——從NestedScrollingParent和NestedScrollingChild分析入手

NestedScrolling機制概述

我們知道,在Android系統對於Touch事件有一套自己的分發機制,其中主要涉及到以下三個方法:

  • dispatchTouchEvent():主要是在View和ViewGroup中進行事件分發

  • onInterceptTouchEvent():進行Touch事件的攔截

  • onTouchEvent():Touch事件的處理

事件分發機制不是這裏所講內容的重點,就不多介紹了。但是這裏分發過程會有個問題,如果子View消費本次事件,父View就沒法再對觸摸事件進行處理,同理,如果父View將事件攔截了,本次觸摸手勢內就無法再講事件分發給子View。這種情況如果如果想要處理父View和子View的嵌套滑動就比較困難了。所以Google又給我們提供了NestedScrolling機制。

所謂的NestedScrolling機制是這樣的:內部NestedScrollingChild在滾動的時候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對其進行部分消耗,Parent處理完後,再將剩餘的部分還給內部NestedScrollingChild。

下面我們就先說下NestedScrollingParent和NestedScrollingChild這兩個類

NestedScrollingParent

廢話不多說,先上源碼

public interface NestedScrollingParent {

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);
    
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
    
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
}

這是一個接口,需要子類實現。看下Google對該類的介紹:

This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.

Classes implementing this interface should create a final instance of a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.

Views invoking nested scrolling functionality should always do so from the relevant ViewCompat, ViewGroupCompat or ViewParentCompat compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.

這裏就簡單翻譯下:

這個接口應該由ViewGroup子類實現,它希望能夠支持由嵌套的子View派發的滾動進行操作操作。

實現類應該應該有一個final的NestedScrollingParentHelper成員屬性,並且將相同簽名的方法委託給NestedScrollingParentHelper執行。

最後一段確實不知道怎麼翻譯,個人理解就是說如果想要兼容的話,就使用兼容庫裏面的ViewCompat,ViewGroupCompat,ViewParentCompat進行相關操作吧。

總結起來就是說,想要實現作爲嵌套滾動的父View,可以實現該類,然後在相應方法裏面做具體的操作。但是需要注意的一點就是在某些方法(同NestedScrollingParentHelper類裏面的同簽名的方法)內,需要調用一下NestedScrollingParentHelper類對應的方法。

說完類,我們再分別看下方法的簡介:

onStartNestedScroll(View child, View target, int nestedScrollAxes)

當NestedScrollingChild調用方法startNestedScroll()時,會調用該方法。主要就是通過返回值告訴系統是否需要對後續的滾動進行處理。返回true:表示我需要進行處理,後續的滾動會觸發相應的回到,false:我不需要處理,後面也就不會進行相應的回調了。參數說明:

child:該ViewParen的包含NestedScrollingChild的直接子View,如果只有一層嵌套,和target是同一個View

target:本次嵌套滾動的NestedScrollingChild

nestedScrollAxes:滾動方向(ViewCompat.SCROLL_AXIS_HORIZONTAL, ViewCompat.SCROLL_AXIS_VERTICAL )

這裏說下child和target的區別,如果是嵌套兩層如:Parent包含一個LinearLayout,LinearLayout裏面纔是NestedScrollingChild類型的View。這個時候,child指向LinearLayout,target指向NestedScrollingChild;如果Parent直接就包含了NestedScrollingChild,這個時候target和child都指向NestedScrollingChild。

onNestedScrollAccepted(View child, View target, int nestedScrollAxes)

如果onStartNestedScroll()方法返回的是true的話,那麼緊接着就會調用該方法.它是讓嵌套滾動在開始滾動之前,讓佈局容器(viewGroup)或者它的父類執行一些配置的初始化的。參數同onStartNestedScroll()一樣。

onStopNestedScroll(@NonNull View target)

停止滾動了,當子view調用stopNestedScroll()時會調用該方法。參數:

target:本次嵌套滾動的NestedScrollingChild

onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed)

當子view調用dispatchNestedScroll()方法時,會調用該方法。也就是開始分發處理嵌套滑動了

target:本次嵌套滾動的NestedScrollingChild

dxConsumed:已經被target消費掉的水平方向的滑動距離

dyConsumed:已經被target消費掉的垂直方向的滑動距離

dxUnconsumed:未被tagert消費掉的水平方向的滑動距離

dyUnconsumed:未被tagert消費掉的垂直方向的滑動距離

onNestedPreScroll(View target, int dx, int dy, int[] consumed)

當子view調用dispatchNestedPreScroll()方法是,會調用該方法。也就是在NestedScrollingChild在處理滑動之前,會先將機會給Parent處理。如果Parent想先消費部分滾動距離,將消費的距離放入consumed。參數說明:

target:本次嵌套滾動的NestedScrollingChild

dx:水平滑動距離

dy:處置滑動距離

consumed:表示Parent要消費的滾動距離,consumed[0]和consumed[1]分別表示父佈局在x和y方向上消費的距離.

onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

你可以捕獲對內部NestedScrollingChild的fling事件,如果return true則表示消費了滑動事件

target:本次嵌套滾動的NestedScrollingChild

velocityX:水平方向的滑動速度

velocityY:垂直方向的滑動速度

consumed:是否被child消費了

onNestedPreFling(View target, float velocityX, float velocityY)

在慣性滑動距離處理之前,會調用該方法,同onNestedPreScroll()一樣,也是給Parent優先處理的權利。返回true:表示Parent要處理本次滑動事件,Child就不要處理了。

target:本次嵌套滾動的NestedScrollingChild

velocityX:水平方向的滑動速度

velocityY:垂直方向的滑動速度

getNestedScrollAxes()

返回當前滑動的方向,一般直接通過NestedScrollingParentHelper.getNestedScrollAxes()返回即可。

NestedScrollingParent介紹的就差不多了,下面我們繼續看NestedScrollingChild

NestedScrollingChild

public interface NestedScrollingChild {
  ​
      void setNestedScrollingEnabled(boolean enabled);
  ​
      boolean isNestedScrollingEnabled();
  ​
      boolean startNestedScroll(int axes);
  ​
      void stopNestedScroll();
  ​
      boolean hasNestedScrollingParent();
  ​
      boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
              int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
  ​
      boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
      
      boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
     
      boolean dispatchNestedPreFling(float velocityX, float velocityY);
  }

老規矩,還是先看下Google官方對該類的介紹:

This interface should be implemented by View subclasses that wish to support dispatching nested scrolling operations to a cooperating parent ViewGroup.

Classes implementing this interface should create a final instance of a NestedScrollingChildHelper as a field and delegate any View methods to the NestedScrollingChildHelper methods of the same signature.

Views invoking nested scrolling functionality should always do so from the relevant ViewCompat, ViewGroupCompat or ViewParentCompat compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.

具體翻譯就不說了,和NestedScrollingParent差不多。只是這裏是嵌套的子View來實現該接口,同時也是需要將指定方法代理給NestedScrollingChildHelper處理。

接下來,看下具體的方法:

setNestedScrollingEnabled(boolean enabled)

啓用或禁用嵌套滾動的方法,設置爲true,並且當前界面的View的層次結構是支持嵌套滾動的(也就是需要NestedScrollingParent嵌套NestedScrollingChild),纔會觸發嵌套滾動。一般這個方法內部都是直接代理給NestedScrollingChildHelper的同名方法即可。

isNestedScrollingEnabled()

判斷當前View是否支持嵌套滑動。一般也是直接代理給NestedScrollingChildHelper的同名方法即可。

startNestedScroll(int axes)stopNestedScroll()

這兩個一般是配對使用的,所以就放在一起講。

startNestedScroll:表示view開始滾動了,一般是在ACTION_DOWN中調用,如果返回true則表示父佈局支持嵌套滾動。一般也是直接代理給NestedScrollingChildHelper的同名方法即可。這個時候正常情況會觸發Parent的onStartNestedScroll()方法

參數axes:滾動方向 ViewCompat.SCROLL_AXIS_HORIZONTALViewCompat.SCROLL_AXIS_VERTICAL.

stopNestedScroll:一般是在事件結束比如ACTION_UP或者ACTION_CANCLE中調用,告訴父佈局滾動結束。一般也是直接代理給NestedScrollingChildHelper的同名方法即可。

hasNestedScrollingParent()

判斷當前View是否有嵌套滑動的Parent。一般也是直接代理給NestedScrollingChildHelper的同名方法即可。

dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

在當前View消費滾動距離之後。通過調用該方法,把剩下的滾動距離傳給父佈局。如果當前沒有發送嵌套滾動,或者不支持嵌套滾動,調用該方法也沒啥用。內部一般也是直接代理給NestedScrollingChildHelper的同名方法即可。返回true:表示滾動事件分發成功,fasle分發失敗。參數:

dxConsumed:被當前View消費了的水平方向滑動距離

dyConsumed:被當前View消費了的垂直方向滑動距離

dxUnconsumed:未被消費的水平滑動距離

dyUnconsumed:未被消費的垂直滑動距離

offsetInWindow:輸出可選參數。如果不是null,該方法完成返回時,會將該視圖從該操作之前到該操作完成之後的本地視圖座標中的偏移量封裝進該參數中,offsetInWindow[0]水平方向,offsetInWindow[1]垂直方向

dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

在當前View消費滾動距離之前把滑動距離傳給父佈局。相當於把優先處理權交給Parent。返回true:代表Parent消費了滾動距離,內部一般也是直接代理給NestedScrollingChildHelper的同名方法即可。參數:

dx:當前水平方向滑動的距離

dy:當前垂直方向滑動的距離

consumed:輸出參數,會將Parent消費掉的距離封裝進該參數consumed[0]代表水平方向,consumed[1]代表垂直方向

offsetInWindow:輸出可選參數。如果不是null,該方法完成返回時,會將該視圖從該操作之前到該操作完成之後的本地視圖座標中的偏移量封裝進該參數中。offsetInWindow[0]水平方向,offsetInWindow[1]垂直方向

dispatchNestedFling(float velocityX, float velocityY, boolean consumed)

將慣性滑動的速度分發給Parent。返回true表示Parent處理了滑動事件。內部一般也是直接代理給NestedScrollingChildHelper的同名方法即可參數:

velocityX:表示水平滑動速度

velocityY:垂直滑動速度

consumed:true:表示當前View消費了滑動事件,否則傳入false

dispatchNestedPreFling(float velocityX, float velocityY)

在當前View自己處理慣性滑動前,先將滑動事件分發給Parent。返回true:表示Parent已經處理了滑動事件。一般來說如果想自己處理慣性的滑動事件,就不應該調用該方法給Parent處理。如果給了Parent並且返回true,那表示Parent已經處理了,自己就不應該再做處理。返回false,代表Parent沒有處理,但是不代表Parent後面就不用處理了,要根據實際情況看是否繼續回調給Parent處理,內部一般也是直接代理給NestedScrollingChildHelper的同名方法即可參數說明:

velocityX:表示水平滑動速度

velocityY:垂直滑動速度

分析完上面兩個類,細心的同學應該發現了一點規律(這裏只討論支持嵌套滾動的情況):

  1. NestedScrollingParent接口中的方法,一般都需要我們根據具體的業務邏輯進行處理,部分方法需要將相關參數代理給NestedScrollingParentHelper。

  2. NestedScrollingChild中的方法,基本上我們重寫的時候只需要代理給NestedScrollingChildHelper即可。而我們關心的重點是在哪裏、什麼時候調用這些方法。

  3. 一般情況下,都會優先將處理權優先交給Parent,根據Parent處理的結果,Child再根據實際情況進行相應處理。

從上面的介紹,我們已經能大概看出整個嵌套滾動的事件分發流程了,先初步畫出時序圖如下:

 

這裏的時序圖暫時只涉及到了NestedScrollingParen和NestedScrollingChild兩個類。只能說大概分析出執行流程,具體中間的調用關係的梳理,就需要用到我們的兩個Helper類了。我們先介紹NestedScrollingParenHelper這個簡單的。

NestedScrollingParenHelper

還是先看源碼

public class NestedScrollingParentHelper {
      private final ViewGroup mViewGroup;
      private int mNestedScrollAxes;
  ​
      public NestedScrollingParentHelper(@NonNull ViewGroup viewGroup) {
          mViewGroup = viewGroup;
      }
  ​
      public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
              @ScrollAxis int axes) {
          onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
      }
  ​
      public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
              @ScrollAxis int axes, @NestedScrollType int type) {
          mNestedScrollAxes = axes;
      }
  ​
      @ScrollAxis
      public int getNestedScrollAxes() {
          return mNestedScrollAxes;
      }
  ​
      public void onStopNestedScroll(@NonNull View target) {
          onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
      }
  ​
      public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
          mNestedScrollAxes = 0;
      }
  }

這個類比較簡單,方法也很簡單,基本上就是一些賦值操作。所以就不多介紹了。這裏就說下它的兩個成員變量:

mViewGroup:用於記錄所關聯的ViewGroup。起始也就是我們需要進程滑動處理的NestedScrollingParent

mNestedScrollAxes:這個就是滑動的方向了。

然後另外需要注意的就是記得在NestedScrollingParent的實現類中,將相同簽名的方法代理給我們的Helper類就可以了。

NestedScrollingChildHelper

這個類纔是真正在我們整個嵌套滾動流程中起着時間分發與傳遞作用的類,還是先看源碼:

public class NestedScrollingChildHelper {
      private ViewParent mNestedScrollingParentTouch;
      private ViewParent mNestedScrollingParentNonTouch;
      private final View mView;
      private boolean mIsNestedScrollingEnabled;
      private int[] mTempNestedScrollConsumed;
  ​
      public NestedScrollingChildHelper(@NonNull View view) {
          mView = view;
      }
  ​
      public void setNestedScrollingEnabled(boolean enabled) {
          if (mIsNestedScrollingEnabled) {
              ViewCompat.stopNestedScroll(mView);
          }
          mIsNestedScrollingEnabled = enabled;
      }
  ​
      public boolean isNestedScrollingEnabled() {
          return mIsNestedScrollingEnabled;
      }
  ​
      public boolean hasNestedScrollingParent() {
          return hasNestedScrollingParent(TYPE_TOUCH);
      }
  ​
      public boolean hasNestedScrollingParent(@NestedScrollType int type) {
          return getNestedScrollingParentForType(type) != null;
      }
  ​
      public boolean startNestedScroll(@ScrollAxis int axes) {
          return startNestedScroll(axes, TYPE_TOUCH);
      }
  ​
      public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
          if (hasNestedScrollingParent(type)) {
              // Already in progress
              return true;
          }
          if (isNestedScrollingEnabled()) {
              ViewParent p = mView.getParent();
              View child = mView;
              while (p != null) {
                  if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                      setNestedScrollingParentForType(type, p);
                      ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                      return true;
                  }
                  if (p instanceof View) {
                      child = (View) p;
                  }
                  p = p.getParent();
              }
          }
          return false;
      }
  ​
      public void stopNestedScroll() {
          stopNestedScroll(TYPE_TOUCH);
      }
  ​
      public void stopNestedScroll(@NestedScrollType int type) {
          ViewParent parent = getNestedScrollingParentForType(type);
          if (parent != null) {
              ViewParentCompat.onStopNestedScroll(parent, mView, type);
              setNestedScrollingParentForType(type, null);
          }
      }
  ​
      
      public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
              int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
          return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                  offsetInWindow, TYPE_TOUCH);
      }
  ​
      
      public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
              int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
              @NestedScrollType int type) {
          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];
                  }
  ​
                  ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                          dyConsumed, dxUnconsumed, dyUnconsumed, type);
  ​
                  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;
      }
  ​
      
      public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
              @Nullable int[] offsetInWindow) {
          return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
      }
  ​
     
      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) {
                      if (mTempNestedScrollConsumed == null) {
                          mTempNestedScrollConsumed = new int[2];
                      }
                      consumed = mTempNestedScrollConsumed;
                  }
                  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;
      }
  ​
      
      public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
          if (isNestedScrollingEnabled()) {
              ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
              if (parent != null) {
                  return ViewParentCompat.onNestedFling(parent, mView, velocityX,
                          velocityY, consumed);
              }
          }
          return false;
      }
  ​
      
      public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
          if (isNestedScrollingEnabled()) {
              ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
              if (parent != null) {
                  return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
                          velocityY);
              }
          }
          return false;
      }
  ​
      
      public void onDetachedFromWindow() {
          ViewCompat.stopNestedScroll(mView);
      }
  ​
      public void onStopNestedScroll(@NonNull View child) {
          ViewCompat.stopNestedScroll(mView);
      }
  ​
      private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
          switch (type) {
              case TYPE_TOUCH:
                  return mNestedScrollingParentTouch;
              case TYPE_NON_TOUCH:
                  return mNestedScrollingParentNonTouch;
          }
          return null;
      }
  ​
      private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
          switch (type) {
              case TYPE_TOUCH:
                  mNestedScrollingParentTouch = p;
                  break;
              case TYPE_NON_TOUCH:
                  mNestedScrollingParentNonTouch = p;
                  break;
          }
      }
  }

老規矩,先看Google對該類的說明:

View subclasses should instantiate a final instance of this class as a field at construction. For each View method that has a matching method signature in this class, delegate the operation to the helper instance in an overridden method implementation. This implements the standard framework policy for nested scrolling.

Views invoking nested scrolling functionality should always do so from the relevant ViewCompat, ViewGroupCompat orViewParentCompat compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.

就不逐句翻譯了,大概翻譯就是一個標準的嵌套滾動的框架策略(說白了就是裏面控制了嵌套滾動的事件分發和一些邏輯處理)。我們實現NestedScrollingChild接口的類裏面應該持有一個該類的成員屬性,作爲對應方法的代理實現。

或者我是這麼理解的,這個類其實就是NestedScrollingChild的標準實現。只是Java沒有多繼承,我們自定義View的時候只能通過實現NestedScrollingChild接口,然後在重寫需要方法中,交個這個標準實現來處理就行了,相當於一種代理模式吧。

當然了,如果你想要搞一套自己的實現策略,你也就可以自己實現每個方法的邏輯,不用交給Helper處理了。

好了,類介紹完了,我們繼續看下類中的方法,都是怎麼實現嵌套滾動策略的。

細心的同學可能已經發現了:

首先這裏面的方法很多都是同NestedScrollingChild一樣的。而且通過上面的講解,知道了,起始這裏的同名方法就是NestedScrollingChild的實現,

其次、這裏面很多方法都是重載的,主要是爲NestedScrollingChild2接口做實現的。而且重載方法就僅僅多了一個type類型。而且我們NestedScrollingChild的實現是沒有type這個類型的方法,調用的時候默認傳了一個TYPE_TOUCH的類型,所以我們後面的介紹,也就只介紹真正有方法實現的那一個,而且針對類型爲TYPE_TOUCH進行介紹。

構造方法

public NestedScrollingChildHelper(@NonNull View view) {
          mView = view;
      }

構造方法很簡單,就是傳一個View進來,該View就是實現了NestedScrollingChild接口的View類型咯。

public void setNestedScrollingEnabled(boolean enabled) {
          if (mIsNestedScrollingEnabled) {
              ViewCompat.stopNestedScroll(mView);
          }
          mIsNestedScrollingEnabled = enabled;
      }

主要用於是給變量mIsNestedScrollingEnabled進行賦值,記錄否可以支持嵌套滾動的方式。可以看到,如果之前是支持嵌套滾動的話,會先調用ViewCompat.stopNestedScroll(mView)停止當前滾動,然後進行賦值操作。

public boolean isNestedScrollingEnabled() {
          return mIsNestedScrollingEnabled;
      }

判斷當前View是否支持嵌套滾動

public boolean hasNestedScrollingParent(@NestedScrollType int type) {
          return getNestedScrollingParentForType(type) != null;
      }
  ​
  private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
          switch (type) {
              case TYPE_TOUCH:
                  return mNestedScrollingParentTouch;
              case TYPE_NON_TOUCH:
                  return mNestedScrollingParentNonTouch;
          }
          return null;
      }

這個主要是獲取嵌套滾動的Parent,也就是實現了NestedScrollingParent的ViewGroup。

繼續下一個

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
          // 先判斷是否已經在嵌套滑動中,是的話,不處理
          if (hasNestedScrollingParent(type)) {
              return true;
          }
          // 判斷是否支持嵌套滑動
          if (isNestedScrollingEnabled()) {
              ViewParent p = mView.getParent();
              View child = mView;
              // 這裏就是利用循環一層一層的網上取出ParentView,直到該ParentView是支持嵌套滑動或者爲null的時候
              while (p != null) {
                  if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { // ①
                      // 給mNestedScrollingParentTouch賦值,後面就可以直接獲取有效ViewParent
                      setNestedScrollingParentForType(type, p); 
                      ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); // ②
                      return true;
                  }
                  if (p instanceof View) {
                      child = (View) p;
                  }
                  p = p.getParent();
              }
          }
          return false;
      }

通過前面的講述,知道當我們調用Child的同名方法的時候,就會進入到該方法(後面所有介紹的方法都一樣,就不再贅述)。

上面註釋①處,就是判斷當前ViewParent是否支持嵌套滑動。同時如果ViewParent是NestedScrollingParent的子類的話,會調用onStartNestedScroll()判斷當前ViewParent是否需要嵌套滑動。

如果註釋①返回true,註釋②裏面就會調用NestedScrollingParent的onNestedScrollAccepted()方法。這也是之前說的,如果要處理嵌套滾動。onStartNestedScroll()必須返回true的原因了。

繼續下一個

public void stopNestedScroll(@NestedScrollType int type) {
          ViewParent parent = getNestedScrollingParentForType(type);
          if (parent != null) {
              // 這裏面就會調用ViewParent的onStopNestedScroll(target, type)方法
              ViewParentCompat.onStopNestedScroll(parent, mView, type);
              // 該次滑動結束 將給mNestedScrollingParentTouch置空
              setNestedScrollingParentForType(type, null);
          }
      }

這個方法裏面已經寫的很清楚了

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
              int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
              @NestedScrollType int type) {
          if (isNestedScrollingEnabled()) {
              // 在startNestedScroll()中進行了賦值操作,所以這裏可以直接獲取ViewParent了
              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];
                  }
                  // 這裏就會調用ViewParent的onNestedScroll()方法
                  ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                          dyConsumed, dxUnconsumed, dyUnconsumed, type);
  ​
                  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;
      }

這個方法就是判斷下,是否是有效的嵌套滑動,是的話調用ViewParent的onNestedScroll()方法,然後return true,否則return false。

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) {
                      if (mTempNestedScrollConsumed == null) {
                          mTempNestedScrollConsumed = new int[2];
                      }
                      consumed = mTempNestedScrollConsumed;
                  }
                  consumed[0] = 0;
                  consumed[1] = 0;
                  // 這裏會調用ViewParent的onNestedPreScroll()方法 Parent消費的數據會縫在consumed變量中
                  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;
      }

這個方法也比較簡單,顯示有效滑動判斷,然後如果正常的話,就會調用ViewParent的onNestedPreScroll()方法。

剩下還有兩個dispathFling相關的方法,全部貼出一起說明。

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
          if (isNestedScrollingEnabled()) {
              ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
              if (parent != null) {
                  // 這裏會調用ViewParent的onNestedFling()方法
                  return ViewParentCompat.onNestedFling(parent, mView, velocityX,
                          velocityY, consumed);
              }
          }
          return false;
      }
  ​
  ​
  public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
          if (isNestedScrollingEnabled()) {
              ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
              if (parent != null) {
                  // 這裏會調用ViewParent的onNestedPreFling()方法
                  return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
                          velocityY);
              }
          }
          return false;
      }

到這裏,關鍵的方法就分析完了。

其實我們發現,NestedScroolingChildHelper中的實現策略,就是在當前Child的所有的祖輩ViewParent中勳在一個實現了NestedScroolingParent接口,並且支持嵌套滾動(onStartNestedScroll()返回true)的。找到之後,在對應的分發方法中,將相關參數分發到ViewParent中與之對應的處理方法中。而且爲了兼容性,都是通過ViewParentCompat進行轉發操作的。

到這裏,整個嵌套滾動的機制就介紹的差不多了。自己也是邊學變整理,所以可能思路不一定非常清晰,介紹也不一定完善,指定有錯誤的地方,就請見諒了。

整個調用的流程圖,這裏就不貼了,有興趣的朋友可以自己畫一下,就算你自己對整個機制的總結,印象肯定比看別人現成來的深刻。

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