导语
在传统的触摸事件分发中,如果不手动调用分发事件或者去发出事件,外部View最先拿到触摸事件,一旦它被外部View拦截消费了,内部View无法接收到触摸事件,同理,内部View消费了触摸事件,外部View也没有机会响应触摸事件(对传统事件分发不了解的,可以去看上一章节的解析 juejin.cn/post/741968…
在此场景下,当多个滑动控件嵌套时,想要做滑动联动效果,就会比较复杂。由此,Android推出了嵌套滑动机制(NestedScrolling),简单来讲,可以让我们在不了解事件分发机制的细节的情况下,优雅的处理复杂的嵌套滑动场景。
原理
将嵌套起来的布局分为父View与子View,子View 在处理滑动事件时建立了询问机制:
- 滑动前询问父View是否需要优先处理
- 子View处理
- 滑动后告知父View自己处理情况,再次询问父View是否需要处理剩余
核心的流程图如下
接口解析 以RecycleView为子View,NestedScrollView为父View为例
public class LogNestRecycleView extends LogRecyclerView{
public LogNestRecycleView(@NonNull Context context) {
super(context);
}
public LogNestRecycleView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public LogNestRecycleView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//嵌套滑动机制 子view
@Override
public boolean startNestedScroll(int axes, int type) {
Log.e("whqq", "子view--startNestedScroll" + "开始嵌套滑动,通过父view.onStartNestedScroll询问父view后续是否要参与滑动的处理");
return super.startNestedScroll(axes, type);
}
/**
* 在滑动之前,将滑动值分发给NestedScrollingParent
* @param dx 水平方向消费的距离
* @param dy 垂直方向消费的距离
* @param consumed 输出坐标数组,consumed[0]为NestedScrollingParent消耗的水平距离、
* consumed[1]为NestedScrollingParent消耗的垂直距离,此参数可空。
* @param offsetInWindow 同上dispatchNestedScroll
* @return 返回NestedScrollingParent是否消费部分或全部滑动值
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
String log = "子view开始滑动前,通过父view.onNestedPreScroll 告知即将滑动的距离dy " + dy +"询问父view是否需要处理";
Log.e("whqq", "子view--dispatchNestedPreScroll " + log);
boolean result = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
return result;
}
/**
* recycle view有一个dispatchNestedScroll 被内部定义为final了,所以打印日志时看不见
* 滑动完成后,将已经消费、剩余的滑动值分发给NestedScrollingParent
* @param dxConsumed 水平方向消费的距离
* @param dyConsumed 垂直方向消费的距离
* @param dxUnconsumed 水平方向剩余的距离
* @param dyUnconsumed 垂直方向剩余的距离
* @param offsetInWindow 含有View从此方法调用之前到调用完成后的屏幕坐标偏移量,
* 可以使用这个偏移量来调整预期的输入坐标(即上面4个消费、剩余的距离)跟踪,此参数可空。
* @return 返回该事件是否被成功分发
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
String log = "子view滑动完成后,通过父view.onNestedScroll 告知子view 以消费距离dxConsumed" + dyUnconsumed + "未消费距离dyUnconsumed " + dyUnconsumed +"询问父view是否需要处理";
Log.e("whqq", "子view--dispatchNestedScroll" + log);
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
}
public class LogNestedScrollView extends NestedScrollView {
public LogNestedScrollView(@NonNull Context context) {
super(context);
}
public LogNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public LogNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 当NestedScrollingChild调用方法startNestedScroll()时,会调用该方法。主要就是通过返
* 回值告诉系统是否需要对后续的滚动进行处理
* child:该ViewParent的包含 NestedScrollingChild 的直接子 View,如果只有一层嵌套,和
* target是同一个View
* target:本次嵌套滚动的NestedScrollingChild
* axes:滚动方向
*
* @return true:表示我需要进行处理,后续的滚动会触发相应的回到
* false: 我不需要处理,后面也就不会进行相应的回调了
*/
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
boolean result = super.onStartNestedScroll(child, target, axes, type);
String log = result ? "我要参与处理" : "我不需要参与处理";
Log.e("whqq", "父亲 onStartNestedScroll " + log);
return result;
}
/**
* 如果onStartNestedScroll()方法返回的是true的话,那么紧接着就会调用该方法.它是让嵌套滚
* 动在开始滚动之前,
* 让布局容器(viewGroup)或者它的父类执行一些配置的初始化的
*/
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
Log.e("whqq", "父亲 onNestedScrollAccepted ");
super.onNestedScrollAccepted(child, target, axes, type);
}
/**
* 当子view调用dispatchNestedPreScroll()方法是,会调用该方法。也就是在
* NestedScrollingChild在处理滑动之前,
* 会先将机会给Parent处理。如果Parent想先消费部分滚动距离,将消费的距离放入consumed
* dx:水平滑动距离
* dy:处置滑动距离
* consumed:表示Parent要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y
* 方向上消费的距离.
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
String log = "即将滑动的距离dy: "+dy+" 父亲需要消费的距离: "+consumed[1];
Log.e("whqq", "父亲 onNestedPreScroll "+log);
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
/**
* 当子view调用dispatchNestedScroll()方法时,会调用该方法。也就是开始分发处理嵌套滑动了
* dxConsumed:已经被target消费掉的水平方向的滑动距离
* dyConsumed:已经被target消费掉的垂直方向的滑动距离
* dxUnconsumed:未被tagert消费掉的水平方向的滑动距离
* dyUnconsumed:未被tagert消费掉的垂直方向的滑动距离
*/
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
@NonNull int[] consumed) {
String log = "已被子view消费距离:"+dyConsumed +" 未被子view消费距离:"+dyUnconsumed;
Log.e("whqq", "父亲 onNestedScroll"+log);
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
}
}
日志打印,方便理解
应用场景
从上述各个接口的含义可知,日常开发处理的场景主要在子view与父view相互询问是否要消费滚动参数的位置,去处理自定义的功能要求
**onNestedPreScroll:**即将滑动的距离,在子view滑动前询问父view是否需要消费,父view可在此处处理滑动事件
**onNestedScroll:**子view消费完滑动事件后,剩余未消费的事件通知父view,父view可在此处理消费剩余滑动事件的逻辑
举例
1、NestedScrollView嵌套RecycleView,手指在RecycleView中滑动,当RecycleView不可滑动时,NestedScrollView 不会跟随滚动
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
@NonNull int[] consumed) {
//子view未能完全消费场景,不通知父亲view,父view收到的未消费滚动事件永远为0,因此不会滚动
int adapterY = 0;
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, adapterY, type, consumed);
}
2、NestedScrollView嵌套RecycleView,手指在RecycleView中滑动,fling操作仅在内部传递,不传递给父view
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
@NonNull int[] consumed) {
int adapterY = dyUnconsumed;
//子view正在消费,但未能完全消费场景,不通知父亲view
if (dyConsumed != 0 && dyUnconsumed != 0) {
adapterY = 0;
}
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, adapterY, type, consumed);
}
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
float adapterY = velocityY;
adapterY = 0;
return super.onNestedFling(target, velocityX, adapterY, consumed);
}
大佬们的文章