public class NestedScrollView extends FrameLayout implements NestedScrollingParent, NestedScrollingChild, ScrollingView
可以发现实现了两个没有见过的接口:NestedScrollingParent, NestedScrollingChild,也是这篇文章的主角。 Android 就是通过这两个接口, 来实现 子 View 与父View 之间的嵌套滑动。这样的嵌套滑动机制是在 Android 发布 Lollipop 之后提供的。不过同样在Support v7 中同样支持了。同时 RecycleView 以及 Android 5.0 以上的系统原声 View 大部分都已经支持嵌套滑动了 。
NestedScrolling 机制能够让父 View 和子 View 在滚动式进行配合,而要实现这样的交互机制,首先父 view 要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口,在这套机制中子 View是发起者,父 view 是接受回调并做出响应的。
四个主要接口
// 主要接口 NestedScrollingChild NestedScrollingParent // 帮助类 NestedScrollingChildHelper NestedScrollingParentHelper
正如前面所说的,很多 view 现在都实现了这两个接口:
-
NestedScrollView 已经实现了 NestedScrollingChild 和 NestedScrollingParent 两个接口
-
RecycleView 已经实现了 NestedScrollingChild
-
CoordinatorLayout 实现了 NestedScrollingParent
那实现这两个接口都需要做哪些工作呢?
要实现 NestedScrollingChild 接口,那么需要重写下面几个方法:
// 开始、停止嵌套滚动 public boolean startNestedScroll(int axes); public void stopNestedScroll(); // 触摸滚动相关 public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); // 惯性滚动相关 public boolean dispatchNestedPreFling(float velocityX, float velocityY); public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); // 设置是否能够滑动 public void setNestedScrollingEnabled(boolean enabled); public boolean isNestedScrollingEnabled();
要实现 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 onNestedPreScroll(View target, int dx, int dy, int[] consumed); public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); // 当惯性嵌套滚动时被调用 public boolean onNestedPreFling(View target, float velocityX, float velocityY); public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
现在我们大概有些了解了,但是这些方法之间的逻辑关系是怎样的呢?
调用流程
整个对应流程是这样
子view | 父view |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
-
在子 view 需要滑动的时候例如 ACTION_DOWN 的时候就要调用 startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL) 方法来告诉父 view 自己要开始滑动了(实质上是寻找能够配合 child 进行嵌套滚动的 parent)。
-
父 view 会收到 onStartNestedScroll 回调从而决定是不是要配合子view做出响应。如果需要配合,此方法会返回 true。继而 onStartNestedScroll()回调会被调用。
-
在滑动事件产生但是子 view 还没处理前可以调用 dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 这个方法把事件传给父 view 这样父 view 就能在onNestedPreScroll 方法里面收到子 view 的滑动信息,然后做出相应的处理把处理完后的结果通过 consumed 传给子 view。
-
dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。
-
如果父 view 需要在子 view 滑动后处理相关事件的话可以在子 view 的事件处理完成之后调用 dispatchNestedScroll 然后父 view 会在 onNestedScroll 收到回调。
-
最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。
过程清楚之后,我们再来看看一些方法的具体的含义,这样更好理解。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;
此方法的前两个参数用于告诉父 View 此次要滚动的距离;而第三第四个参数用于子 view 获取父 view 消费掉的距离和父 view 位置的偏移量。consumed
参数是一个 int
型的数组,长度为 2,第一个元素是父 view 消费的x
方向的滚动距离;第二个元素是父 view 消费的 y
方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。
如果parent消费了一部分或全部距离,则此方法返回true。
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
返回值: true if the event was dispatched, false if it could not be dispatched.
public void stopNestedScroll();
最后,stopNestedScroll()方法与startNestedScroll(int axes)对应,用于结束嵌套滚动流程;而惯性滚动相关的两个方法与触摸滚动相关的两个方法类似,这里不再赘述。
这里看三个例子,
1、https://github.com/543441727/MyNestedScrolling
2、这个是我改编的上面一个人写的代码,让它能够像下拉刷新那样工作:
https://github.com/huanshen/NestedScrollingParent-Child
3、https://github.com/qstumn/RefreshLayout
简单来说:
在 NestedScrollingChild 处理点击事件,然后发起滚动请求;
在 NestedScrollingParent 的 onNestedPreScroll 处理事件,是不是要自己消耗;
对于同时这两个接口的ViewGroup,需要在这两个里面都进行处理。如果子类是listview,则可以通过点击事件的拦截来进行控制,如果是recyclerView ,那么就不是点击事件的控制了。
NestedScrollingParent, NestedScrollingChild 详解
Android -- NestedScrolling滑动机制