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 dispatchNestedPreFling(float velocityX, float velocityY);
// 设置是否能够滑动
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是否还要进行余下 (unconsumed) 的滚动。
前四个参数为输入参数,用于告诉父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 ,那么就不是点击事件的控制了。