NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动

你也许没见过 NestedScrollingParent 和NestedScrollingChild这两个接口,但你或多或少听过嵌套滑动。就像下图一样,顶部随着下滑出现,上滑隐藏。如果使用传统的事件分发来写的话,不仅复杂还容易出错。

而使用NestedScrollingParent 和NestedScrollingChild来实现的话就简单多了,虽然本质也是基于事件分发,但是谷歌爸爸已经帮我们都封装好了。NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动
文章图片

那么 NestedScrollingParent 和NestedScrollingChild是怎么实现滑动的联动的呢。说到底,这两个只是个接口,还等待我们去实现。别担心,我们只需要定义两个view实现这两个接口,而两个接口的关联类已经有了。现在先来了解一下这两个接口。
既然有Parent和Child,我就可以理解为分别和父视图和子视图相关。具体来说,就是通过捕捉实现了NestedScrollingChild的view的事件,来通知实现了NestedScrollingParent 的view去做点什么事
我们先来看NestedScrollingChild

public interface NestedScrollingChild { /** * 设置是否允许嵌套滑动,允许的话设为true */ public void setNestedScrollingEnabled(boolean enabled); /* * 是否允许嵌套滑动 */ public boolean isNestedScrollingEnabled(); /** * 开始嵌套滑动 */ public boolean startNestedScroll(int axes); /** * 结束嵌套滑动 */ public void stopNestedScroll(); /** * 判断NestedScrollingParent 的onStartNestedScroll方法是否返回true,只有true,才能继续一系列的嵌套滑动 */ public boolean hasNestedScrollingParent(); /** * 子view消费了拖动事件之后通知父view, */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); /** *子view消费了拖动事件之前通知父view,dx dy是将要消费的距离,如果父view要消费可通过 *设置consumed[0]=x .consumed[1]=y来分别消费x,y。然后子view继续处理剩下的位移(即dx-x,dy-y) */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); /** *子view消费了滑动事件之后通知父view, */ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); /** *子view消费了滑动事件之前通知父view */ public boolean dispatchNestedPreFling(float velocityX, float velocityY); }

再看看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(); }

【NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动】有没有发现两个接口很相似,只是把dispatch开头的换成了on开头。
一般来说on开头的方法代表的都是回调方法,我以startNestedScroll方法为例。当子view开始滑动时,会调用startNestedScroll方法,在该方法里面,主要实现就是获取当前的view的父view,如果父view实现了NestedScrollingParent 接口,就会回调onStartNestedScroll。然后在这些回调实现一些界面的变动,就有联动的效果了。
前面说了,两个视图的关联,谷歌已经帮我们实现好了。就是NestedScrollingChildHelper和NestedScrollingParentHelper。
NestedScrollingParentHelper的实现比较简单,就只是保存滑动的方向,事实上不使用它也没什么关系
public class NestedScrollingParentHelper { private final ViewGroup mViewGroup; private int mNestedScrollAxes; /** * Construct a new helper for a given ViewGroup */ public NestedScrollingParentHelper(ViewGroup viewGroup) { mViewGroup = viewGroup; }public void onNestedScrollAccepted(View child, View target, int axes) { mNestedScrollAxes = axes; }public int getNestedScrollAxes() { return mNestedScrollAxes; }public void onStopNestedScroll(View target) { mNestedScrollAxes = 0; } }

而NestedScrollingChildHelper的作用就是负责关联两个view了
当子view开始滑动的使用,会调用NestedScrollingChildHelper的startNestedScroll方法,来启动嵌套滑动。
从下面的代码可以知道,NestedScrollingChild 的startNestedScroll会回调NestedScrollingParent的onStartNestedScroll
后续的几个方法基本也是这种调用模式
public boolean startNestedScroll(int axes) { //验证一个滑动流程只可以startNestedScroll一次 if (hasNestedScrollingParent()) { // Already in progress return true; } //启动滑动,子view要调用setNestedScrollingEnabled方法启动。 if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { //判断父view 是否实现NestedScrollingParent 且接口的onStartNestedScroll方法返回true if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; //调用NestedScrollingParent 的onNestedScrollAccepted回调方法 ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }

从两个接口和NestedScrollingChildHelper和NestedScrollingParentHelper,我发现,虽然NestedScrollingChildHelper和NestedScrollingParentHelper没有实现两个接口,但是定义的方法名字是一样的。这种代理模式不但便于理解,也更好的解耦
通过这四个东西,就可以实现一些联动效果了,NestedScrollingChild 的实现类有很多,比如recyclerview和nestscrollview。一般我们都是基于这些视图滚动进行关联。
下面我定义一个粉色条,根据scrollview的上下滚动而进行上下滑动。效果如下
NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动
文章图片

实现起来其实很简单,因为NestedScrollView实现了NestedScrollingChild接口,NestedScrollView里面又根据滑动情况已经处理好了NestedScrollingChildHelper的调用,在这里,我只要专注NestedScrollingParent 的实现就可以了
.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.example.administrator.transdemo.ScrollingActivity" tools:showIn="@layout/activity_scrolling">.support.v4.widget.NestedScrollView>

ParentView 的实现如下
public class ParentView extends FrameLayout implements NestedScrollingParent2 { public ParentView(@NonNull Context context) { this(context, null); }public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }public ParentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }View imageRight; @Override protected void onFinishInflate() { super.onFinishInflate(); imageRight= getChildAt(1); }@Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { //如果是竖直方向滑动,就启动嵌套滑动 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }@Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {}@Override public void onStopNestedScroll(@NonNull View target, int type) { }@Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { //这里的Consumed代表NestScrollView消耗的距离, Unconsumed代表NestScrollView未消耗的距离 //imageRight根据NestScrollView滑动的距离而进行相应的滑动、。 imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed); }@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {} }

只需要几行代码就实现了两个view的关联滑动。
下面如果我加多一个关联的view,代码如下
public class ParentView extends FrameLayout implements NestedScrollingParent2 { public ParentView(@NonNull Context context) { this(context, null); }public ParentView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }public ParentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }View imageLeft; View imageRight; @Override protected void onFinishInflate() { super.onFinishInflate(); imageLeft = getChildAt(1); imageRight = getChildAt(2); }@Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }@Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {}@Override public void onStopNestedScroll(@NonNull View target, int type) {}@Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { imageRight.setTranslationY(imageRight.getTranslationY() + dyConsumed); }@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) { imageLeft.setTranslationY(imageLeft.getTranslationY() + dy); } }

NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动
文章图片

imageLeft表示左边的条块,imageRight表示右边的,滑动scrollview的时候,会怎么变化?
答案如下图
NestedScrollingParent 和NestedScrollingChild 实现嵌套滑动
文章图片

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) { //dx dy表示ontouEvent move的时候产生的原始偏移值,NestedScrollView处理它自己的滑动之前先调用这个方法。 //因为没有调用consumed[1] = xx,消耗掉相应的偏移值,所以他和右边滑块的速度是一样的。 //而NestedScrollView滑动到顶部的,继续上滑,ontouEvent move照样调用,该方法继续触发,下面的语句继续执行 //而imageRight 是跟随NestedScrollView偏移值进行相应的偏移,所以imageRight 不会动。 imageLeft.setTranslationY(imageLeft.getTranslationY() + dy); }

上面我使用了NestedScrollingParent2 而不是NestedScrollingParent,其实NestedScrollingParent2 继承了NestedScrollingParent接口,只是在下面方法后面加多了一个参数 @NestedScrollType int type ,该参数有两张情况TYPE_TOUCH, TYPE_NON_TOUCH,表示当前状态是否在手指触摸下
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onStopNestedScroll(@NonNull View target, @NestedScrollType int type); void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type); void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, @NestedScrollType int type);

NestedScrollingParent 和NestedScrollingChild应用无处不在。使用最多的就是CoordinatorLayout。
CoordinatorLayout + AppBarLayout实现的toolbar滑动就是使用这个原理实现的。当前CoordinatorLayout不止是有嵌套滑动这个效果。但是目前很多酷炫的滑动特效都是基于CoordinatorLayout 实现的

    推荐阅读