嵌套滑动具体点就是当子View有滑动的时候把滑动的情况告诉父View,让父View也可以做一些相应的滑动动作。父View做完动作之后又把滑动的情况告诉子View。可以看出子View是主动的一方,父View是被动的一方(父View也可能是父父View这里为了方便一点就直接说的是父View)。
分两部分来介绍嵌套滑动。一个是简单的从源码的层次分析嵌套滑动,一个是具体的实例。
源码层次分析嵌套滑动
要分析嵌套滑动的具体实现得先知道嵌套滑动四个相关的类NestedScrollingChild,NestedScrollingChildHelper,NestedScrollingParent,NestedScrollingParentHelper。
NestedScrollingChild:在子View中要实现的接口。NestedScrollingChild函数的相关解释。
public interface NestedScrollingChild {
// 设置是否允许嵌套滑动
public void setNestedScrollingEnabled(boolean enabled);
// 是否允许嵌套滑动
public boolean isNestedScrollingEnabled();
/** 告诉开始嵌套滑动流程,调用这个函数的时候会去找嵌套滑动的父控件。如果找到了父控件并且父控件说可以滑动就返回true,否则返回false
* (一般ACTION_DOWN里面调用)
* @param axes:支持嵌套滚动轴。水平方向,垂直方向,或者不指定
* @return true 父控件说可以滑动,false 父控件说不可以滑动
*/
public boolean startNestedScroll(int axes);
// 停止嵌套滑动流程(一般ACTION_UP里面调用)
public void stopNestedScroll();
// 是否有嵌套滑动对应的父控件
public boolean hasNestedScrollingParent();
/**
* 在嵌套滑动的子View滑动之后再调用该函数向父View汇报滑动情况。
* @param dxConsumed 子View水平方向滑动的距离
* @param dyConsumed 子View垂直方向滑动的距离
* @param dxUnconsumed 子View水平方向没有滑动的距离
* @param dyUnconsumed 子View垂直方向没有滑动的距离
* @param offsetInWindow 出参 如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
* 该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化
* 如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
* @return true 如果父View有滑动做了相应的处理, false 父View没有滑动.
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 在嵌套滑动的子View滑动之前,告诉父View滑动的距离,让父View做相应的处理。
* @param dx 告诉父View水平方向需要滑动的距离
* @param dy 告诉父View垂直方向需要滑动的距离
* @param consumed 出参. 如果不是null, 则告诉子View父View滑动的情况, consumed[0]父View告诉子View水平方向滑动的距离(dx)
* consumed[1]父View告诉子View垂直方向滑动的距离(dy).
* @param offsetInWindow 可选 length=2的数组,如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
* 该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化
* 如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
* @return true 父View滑动了,false 父View没有滑动。
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 在嵌套滑动的子View fling之后再调用该函数向父View汇报fling情况。
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed true 如果子View fling了, false 如果子View没有fling
* @return true 如果父View fling了
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 在嵌套滑动的子View fling之前告诉父View fling的情况。
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @return 如果父View fling了
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
NestedScrollingChildHelper:子View嵌套滑动的帮助类,在子View实现的接口NestedScrollingChild里面简单掉下NestedScrollingChildHelper里面的对应的方法就好了,用于告诉嵌套滑动对应的父View相关的嵌套事件。
public class NestedScrollingChildHelper {
private final View mView;
private ViewParent mNestedScrollingParent;
private boolean mIsNestedScrollingEnabled;
private int[] mTempNestedScrollConsumed;
/**
* 通过给定的子View构造NestedScrollingChildHelper
*/
public NestedScrollingChildHelper(View view) {
mView = view;
}
/**
* 设置是否允许嵌套滑动(如果当前View已经设置了嵌套滑动则会调用到当前View的stopNestedScroll)
* @param enabled true 是否允许嵌套滑动
*/
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
/**
* 检查是否允许嵌套滑动
*/
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
/**
* 检查是否有嵌套滑动对于的父View
*/
public boolean hasNestedScrollingParent() {
return mNestedScrollingParent != null;
}
/**
* 告诉父View准备开始嵌套滑动(这个函数做的事情就是去找嵌套滑动对应父View并且判断该父View是否接收嵌套滑动的事件)
* 讲道理的话该函数会调用到嵌套对应的父View的onStartNestedScroll 和 onNestedScrollAccepted函数
* @param axes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
* @return true 找到了嵌套滑动的父View,并且父View会接受嵌套滑动事件。
*/
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
/**
* 告诉嵌套滑动对于的父View嵌套滑动结束
* 讲道理的话该函数会调用到嵌套对应的父View的onStopNestedScroll函数
*/
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
/**
* 在子View处理了滑动动作之后告诉嵌套滑动对于的父View子View的滑动情况。参数和NestedScrollingChild里面的对应。
* 讲道理的话该函数会调用到嵌套对应的父View的onNestedScroll函数
* 这里注意下offsetInWindow这个参数是个出参 是子View位置的变化值。这个参数的变化值,不用我们在嵌套滑动的父View里面去设置,在这个函数里面已经设置了。
* @return true 如果嵌套滑动对应的父View有滑动
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
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(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
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;
}
/**
* 在子View处理滑动事件之前,告诉嵌套滑动对应的父View滑动的情况。
* 参数的意思和NestedScrollingChild里面的对应
* 讲道理的话该函数会调用到嵌套对应的父View的onNestedPreScroll函数
* 同时也可以看到offsetInWindow不用我们在父View里面去设置,但是consumed这个参数是要我们在父View里面去设置的。
* @return true 嵌套滑动对应的父View有滑动。
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
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(mNestedScrollingParent, mView, dx, dy, consumed);
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;
}
/**
* 子View fling之后把fling的情况报告给嵌套滑动对应的父View
* 讲道理的话该函数会调用到嵌套对应的父View的onNestedFling函数
* @return true 嵌套滑动对于的父View fling了
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
velocityY, consumed);
}
return false;
}
/**
* 子View fling之前 告诉嵌套滑动对应的父View fling的情况
* 讲道理的话该函数会调用到嵌套对应的父View的onNestedPreFling函数
* @return true 嵌套滑动对应的父View消耗了fling事件
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
velocityY);
}
return false;
}
/**
* 当子View脱离窗口的时候调用该函数告知停止嵌套滑动
* 该函数会调用到当前View(嵌套滑动的子View)的stopNestedScroll函数
*/
public void onDetachedFromWindow() {
ViewCompat.stopNestedScroll(mView);
}
/**
* 告知停止嵌套滑动
* 该函数会调用到当前View(嵌套滑动的子View)的stopNestedScroll函数
*/
public void onStopNestedScroll(View child) {
ViewCompat.stopNestedScroll(mView);
}
}
这里NestedScrollingChildHelper里面的函数是怎么和嵌套滑动的父View建立联系的。每个函数的处理方式基本上都是通过ViewCompat的帮助来实现的。
第45行,NestedScrollingChildHelper类的startNestedScroll函数,该函数是用于告诉嵌套滑动对应的父View嵌套滑动要开始了,函数里面有个while循环一级一级的遍历父View去找嵌套滑动对应的父View。while前面都是一些判断性的语句直接看54行 ViewParentCompat.onStartNestedScroll(p, child, mView, axes),该函数的实现在ViewParentCompat的内部类ViewParentCompatStubImpl里面具体代码如下
@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
就是判断panrent是否实现了NestedScrollingParent接口(所以说父View一定要实现NestedScrollingParent接口)。调用parent的onStartNestedScroll方法,到这里处理逻辑就到parent里面去了,给parent自己处理了。继续看NestedScrollingChildHelper的startNestedScroll函数第56行ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);通过这句的调用也是直接调用了parent的onNestedScrollAccepted方法。到此startNestedScroll函数分析结束。
这里可以看出NestedScrollingChildHelper类的startNestedScroll函数会调用到parent的onStartNestedScroll和onNestedScrollAccepted方法。
第85行 NestedScrollingChildHelper类的dispatchNestedScroll和其他的函数处理逻辑是一样的调用到parent的onNestedPreScroll函数,并且这里我们也可以看出onNestedScroll每个参数的意义。同时在这里也可以看到dispatchNestedScroll的最后一个参数这里已经帮我们算好了,里面放的是子View位置的变化。
第122行 NestedScrollingChildHelper类的dispatchNestedPreScroll函数调用了parent的onNestedPreScroll函数。看到dispatchNestedPreScroll的倒数第一个参数offsetInWindow已经帮我们处理好了,但是倒数第二个参数consumed却没有帮我们处理,我们要在parent的onNestedPreScroll处理赋值。
NestedScrollingChildHelper的其他的函数也基本上是这样的流程 都是要不调用到嵌套滑动对于的父View里面的函数(NestedScrollingParent里面的某个函数),要不就是调用到子View里面的函数(NestedScrollingChild里面的某个函数)
NestedScrollingParent:父View要实现的接口。相关函数的意义如下
public interface NestedScrollingParent {
/**
* 有嵌套滑动到来了,问下该父View是否接受嵌套滑动
* @param child 嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target)
* @param target 具体嵌套滑动的那个子类
* @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
* @return 是否接受该嵌套滑动
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* 该父View接受了嵌套滑动的请求该函数调用。onStartNestedScroll返回true该函数会被调用。
* 参数和onStartNestedScroll一样
*/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* 停止嵌套滑动
* @param target 具体嵌套滑动的那个子类
*/
public void onStopNestedScroll(View target);
/**
* 嵌套滑动的子View在滑动之后报告过来的滑动情况
*
* @param target 具体嵌套滑动的那个子类
* @param dxConsumed 水平方向嵌套滑动的子View滑动的距离(消耗的距离)
* @param dyConsumed 垂直方向嵌套滑动的子View滑动的距离(消耗的距离)
* @param dxUnconsumed 水平方向嵌套滑动的子View未滑动的距离(未消耗的距离)
* @param dyUnconsumed 垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离)
*/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* 在嵌套滑动的子View未滑动之前告诉过来的准备滑动的情况
* @param target 具体嵌套滑动的那个子类
* @param dx 水平方向嵌套滑动的子View想要变化的距离
* @param dy 垂直方向嵌套滑动的子View想要变化的距离
* @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离
* consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
*/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 嵌套滑动的子View在fling之后报告过来的fling情况
* @param target 具体嵌套滑动的那个子类
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @param consumed 子view是否fling了
* @return true 父View是否消耗了fling
*/
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
/**
* 在嵌套滑动的子View未fling之前告诉过来的准备fling的情况
* @param target 具体嵌套滑动的那个子类
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @return true 父View是否消耗了fling
*/
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
/**
* 获取嵌套滑动的轴
* @see ViewCompat#SCROLL_AXIS_HORIZONTAL 垂直
* @see ViewCompat#SCROLL_AXIS_VERTICAL 水平
* @see ViewCompat#SCROLL_AXIS_NONE 都支持
*/
public int getNestedScrollAxes();
}
NestedScrollingParentHelper:父View嵌套滑动的帮助类,这个类就简单了
public class NestedScrollingParentHelper {
private final ViewGroup mViewGroup;
private int mNestedScrollAxes;
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;
}
}
四个重要的类NestedScrollingChild,NestedScrollingChildHelper,NestedScrollingParent,NestedScrollingParentHelper。大概的知道了每个函数的意思。从上面的分析也可以看出子View是主动的一方,把相关的滑动信息通过NestedScrollingChildHelper告诉父View让父View做相应的处理。接下来就是该怎么使用了。
嵌套滑动使用实例
嵌套滑动具体使用。注意只能子View通知父View,父View里面的嵌套滑动都是被动调用的。
嵌套滑动子View 一般使用方法(用scroll来举例)
1. 实现NestedScrollingChild接口。
2. 定义NestedScrollingChildHelper变量。
3. 在实现的NestedScrollingChild每个接口中调用。NestedScrollingChildHelper对应的函数。
4. setNestedScrollingEnabled(true); 一般在初始化里面调用设置可以嵌套滑动。
5. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_DOWN 调用startNestedScroll函数 告诉父View开始嵌套滑动。
6. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_MOVE 调用dispatchNestedPreScroll或者dispatchNestedScroll 这个就视情况而定了告诉父View滑动的情况。
7. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_UP 调用stopNestedScroll 告诉父View结束嵌套滑动。
8. 重写onDetachedFromWindow方法,调用NestedScrollingChildHelper的onDetachedFromWindow方法
嵌套滑动父View 一般使用方法
1. 实现NestedScrollingParent接口。
2. 定义NestedScrollingParentHelper变量。
3. 在实现的NestedScrollingParent几个接口中(onNestedScrollAccepted, onStopNestedScroll, getNestedScrollAxes)调用NestedScrollingParentHelper对应的函数。
4. 视情况而定onNestedScroll onNestedPreScroll onNestedFling onNestedPreFling 做相应的处理。
给出一个具体demo,当子View滑动的时候碰到了父View边缘的时候让父View也一起滑动。例子的代码来源http://blog.csdn.net/zty19901005/article/details/50974388
demo下载地址
注:上面借助NestedScrollingChild NestedScrollingChildHelper NestedScrollingParent NestedScrollingParentHelper这四个类只能做到子View通知父View。但有的时候仅仅只是通知到父View还满足不了我们的需求。最好有一种方法通知到了父View然后在通知到兄弟View。这个时候就该是Behavior出场了。借助Behavior可以帮助父View通知到兄弟View。比如 CoordinatorLayout + AppBarLayout + Toolbar + RecyclerView 实现RecyclerView上滑隐藏Toolbar下滑显示Toolbar的效果
要实现这个效果分两步
1. RecyclerView 通知到 CoordinatorLayout :RecyclerView肯定实现了NestedScrollingChild,CoordinatorLayout实现了NestedScrollingParent。
2. CoordinatorLayout 通知到 AppBarLayout :AppBarLayout 里面肯定有一个Behavior。CoordinatorLayout 在onNestedPreScroll函数里面会去找CoordinatorLayout子View 是否有Behavior。然后把情况告诉Behavior。
关于Behavior的简单实现以后再分析。