解决嵌套滑动
事件分发:子View首先得到事件处理权,处理过程中,父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。
NestedScrolling:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。
事件分发,对于拦截相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给Parent(拦截者)来处理。使用ViewGroup事件分发处理实现某些效果,需要手动调用分发事件、手动发出事件。
嵌套滑动存在两个角色:子View和父View(注:一个View可以同时扮演2个角色,子View并不一定是父View的直接子View)
对每一次MOVE事件传递来的滑动,都使用「parent -> child -> parent -> child」机制进行消费,让子View在消费滑动时与父View配合更加细致、紧密和灵活
NestedScrolling 接口分为两个部分:NestedScrollingParent 及 NestedScrollingChild。
public interface NestedScrollingParent {
/**
* 开始NestedScroll时调用
* 对子孙View开始滑动请求的回应(NestedScrollingChild.startNestedScroll)
* 返回true就意味着后面可以接受到NestedScroll事件,否则就无法接收。
*
* @param child 该view的直接子view(包含发起请求的子孙View)
* @param target 发出NestedScroll事件的子孙View,和child不一定是同一个
* @param nestedScrollAxes 滑动的方向,为ViewCompat#SCROLL_AXIS_HORIZONTAL或者ViewCompat#SCROLL_AXIS_VERTICAL,亦或两者都有。
* @return 返回true代表要消耗这个NestedScroll事件,否则就是false。
* */
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* 在onStartNestedScroll之后调用
* 对开始滑动响应的回调(onStartNestedScroll返回true之后会有此回调产生)
* 参数意义同上
* 使NestedScrollingParent有做滑动初始化工作的时机
* */
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* 终止nestedscroll的回调(NestedScrollingChild调用stopNestedScroll)
*
* @param target 发出NestedScroll事件的子孙View
* */
public void onStopNestedScroll(View target);
/**
* 在target每次滑动之前会调用这个方法
* 在NestedScrollingChild处理滑动之前,预处理此滑动
*
* consumed是一个长度为2的数组。
* 第0位时我们在x方向消耗的滑动距离
* 第1位是我们在y方向上消耗的滑动距离
* 子view会根据这个和dx/dy来计算余下的滑动量,来决定自己是否还要进行剩下的滑动。
* 比如我们使consumed[1] = dy,那么子view在y方向上就不会滑动。
*
* @param target 发出NestedScroll事件的子孙View
* @param dx 这次滑动事件在x方向上滑动的距离
* @param dy 这次滑动事件在y方向上滑动的距离
* @param consumed 回填参数,填入此次预处理消耗掉的滑动距离
* */
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 在target滑不动的时候会调用这个方法
* 处理NestedScrollingChild未消耗完的滑动距离。
* 如果目标view可以一直滑动,那么这个方法就不会被调用
*
* @param target 发出NestedScroll事件的子孙View
* @param dxConsumed 已消耗的x轴滑动距离
* @param dxUnconsumed 未消耗的x轴滑动距离
* @param dyConsumed 已消耗的y轴滑动距离
* @param dyUnconsumed 未消耗的y轴滑动距离
*
* */
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* 在target判断为fling并且执行fling之前调用,我们可以通过返回true来拦截目标的fling,这样它就不会执行滑动。
* @param target 发出请求的子孙View
* @param velocityX 在x方向的起始速度
* @param velocityY 在y方向的起始速度
* @return 我们是否消耗此次fling,返回true代表拦截,返回false,目标view就进行正常的fling
* */
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
/**
* 在target进行fling后调用。注意这个方法并不是像onNestedScroll在子view滑不动之后调用,而是紧跟着onNestedPreFling后会被调用。因此对于它的使用场景一般比较少。
*
* @param target 目标view
* @param velocityX 在x方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityX是一样的。
* @param velocityY 在y方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityY是一样的。
* @param consumed 目标view是否消耗了此次fling
* @return 本view是否消耗了这次fling,return true会拦截掉内部View的事件
* */
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
/**
* 获取滑动方向
* SCROLL_AXIS_NONE = 0 没有滑动方向
* SCROLL_AXIS_HORIZONTAL = 1 << 0 横向滑动
* SCROLL_AXIS_VERTICAL = 1 << 1 纵向滑动
* @return 滑动方向
*/
public int getNestedScrollAxes();
}
public interface NestedScrollingChild {
/**
* 设置当前View是否启用NestedScroll特性,一般设置为true
* @param enabled 是否启用
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 判断当前View是否启用了NestedScroll特性
* @return 是否启用
*/
boolean isNestedScrollingEnabled();
/**
* 在axes轴上发起NestedScroll开始操作
* @param axes 滑动方向
* @return 是否有NestedScrollingParent接受此次滑动请求,如果不接受返回false,后续的嵌套滑动都将失效
*/
boolean startNestedScroll(@ViewCompat.ScrollAxis int axes);
/**
* 终止NestedScroll
*/
void stopNestedScroll();
/**
* 当前是否有NestedScrollingParent接受了此次滑动请求
* @return 是否有NestedScrollingParent接受此次滑动请求
*/
boolean hasNestedScrollingParent();
/**
* NestedScroll滑动操作中,在自己开始滑动处理之前,分配预处理操作(一般为询问NestedScrollingParent是否消耗部分滑动距离)
* @param dx 当前这一步滑动的x轴总距离(相对于上一次事件,而不是相对于DOWN事件)
* @param dy 当前这一步滑动的y轴总距离
* @param consumed 预处理操作消耗掉的距离(此为输出参数,consumed[0]为预处理操作消耗掉的x轴距离,consumed[1]为预处理操作消耗掉的y轴距离)
* @param offsetInWindow 可选参数,可以为null。为输出参数,获取预处理操作使当前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分别为x轴和y轴偏移)
* @return 预处理操作是否消耗了部分或全部滑动距离
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
* 在当前View处理了滑动之后继续分配滑动操作 (一般在自己处理滑动之后,给NestedScrollingParent机会处理剩余的滑动距离)
* @param dxConsumed 已经消耗了的x轴滑动距离
* @param dyConsumed 已经消耗了的y轴滑动距离
* @param dxUnconsumed 未消耗的x轴滑动距离
* @param dyUnconsumed 未消耗的y轴滑动距离
* @param offsetInWindow 可选参数,可以为null。为输出参数,获取预处理操作使当前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分别为x轴和y轴偏移)
* @return 预处理操作是否消耗了部分或全部滑动距离
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* 在当前NestedScrollingChild处理fling事件之前进行预处理(一般询问NestedScrollingParent是否处理消耗此次fling)
* @param velocityX x轴速度
* @param velocityY y轴速度
* @return 预处理是否处理消耗了此次fling
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
/**
* 分配fling操作
* @param velocityX x轴方向速度
* @param velocityY y轴方向速度
* @param consumed 当前NestedScrollingChild是否处理了此次fling
* @return NestedScrollingParent是否处理了此次fling
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
}
NestedScrollingChild | NestedScrollingParent |
---|---|
startNestedScroll | onStartNestedScroll |
onNestedScrollAccepted | |
stopNestedScroll | onStopNestedScroll |
dispatchNestedScroll | onNestedScroll |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedFling | onNestedFling |
dispatchNestedPreFling | onNestedPreFling |
getNestedScrollAxes |
针对一次滑动操作,子View接口调用顺序为:
startNestedScroll -> dispatchNestedPreScroll -> dispatchNestedScroll -> stopNestedScroll
子View | 父View | 备注 |
---|---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted | 如果onStartNestedScroll返回true,则调用onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll | |
dispatchNestedScroll | onNestedScroll | |
stopNestedScroll | onStopNestedScroll |
在 Android 5.0 / API 21 (2014.9) 时, Google 第一次加入了 NestedScrolling 机制。
因为第一个版本的 NestedScrolling 机制是加在 framework 层的 View 和 ViewGroup 中,所以能享受到嵌套滑动效果的只能是Android 5.0的系统,也就是当时最新的系统。 所以在当时 NestedScrolling 机制基本没有怎么被使用。
Google重构出来两个接口(NestedScrollingChild、NestedScrollingParent)两个 Helper (NestedScrollingChildHelper、NestedScrollingParentHelper)外加一个开箱即用的NestedScrollView,在 Revision 22.1.0 (2015.4) 到来之际,把它们一块加入了v4 support library。Revision 22.2.0 (2015.5)时,Google又隆重推出了 Design Support library,其中的杀手级控件CoordinatorLayout更是把 NestedScrolling 机制玩得出神入化。
变化
把 NestedScrolling 机制从 View 和 ViewGroup 中剥离,把有关的 API 放在接口中,把相关实现放在 Helper 里
优点
低版本的 View 也能嵌套滑动
缺点
使用麻烦,暴露了更多内部的不需要普通使用者关心的 API,影响开发者对整个机制的上手速度
惯性不连续
在滑动内部 View 时快速抬起手指,内部 View 会开始惯性滑动,当内部 View 惯性滑动到自己顶部时便停止了滑动,此时外部的可滑动 View 不会有任何反应,即使外部 View 可以滑动。
2017年9月,Revision 26.1.0更新了一版NestedScrollingChild2和NestedScrollingParent2,并且处理了第一版中系统控件的Bug,区分开是用户手指移动触发的滑动还是由惯性触发的滑动
当外部 View 在顶部、内部 View 也在顶部时,往下滑动内部 View 然后快速抬起(制造 fling ),再马上滑外部 View
预期应该是:外部 View 往上滚动
但实际上:滑不动它,或是滑上去一点,马上又下来了
2018年11月5日androidx.core 1.1.0-alpha01的更新中,给出了最新的修复——NestedScrollingChild3和NestedScrollingParent3
当通过滑动内部 View 触发外部 View 滑动时,无法通过触摸外部 View 把它停下来
https://blog.csdn.net/lmj623565791/article/details/52204039
https://juejin.im/post/5c3c8d2ae51d4552475fcef7
https://blog.csdn.net/zuguorui/article/details/78671480
https://blog.csdn.net/tobacco5648/article/details/84667016
https://blog.csdn.net/dqh147258/article/details/81208889
https://blog.csdn.net/chen930724/article/details/50307193
https://blog.csdn.net/lmj121212/article/details/52974427
https://blog.csdn.net/happy_horse/article/details/54619526
https://blog.csdn.net/fyfcauc/article/details/52415144