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进行转发操作的。

到这里,整个嵌套滚动的机制就介绍的差不多了。自己也是边学变整理,所以可能思路不一定非常清晰,介绍也不一定完善,指定有错误的地方,就请见谅了。

整个调用的流程图,这里就不贴了,有兴趣的朋友可以自己画一下,就算你自己对整个机制的总结,印象肯定比看别人现成来的深刻。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值