对于Android的事件分发流程,大家都知道主要有三个函数:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,这三个函数将我们的touch事件按照一定的规则传递给相应的view,完成了整个的事件交互。但是这套事件处理机制有一个缺点:如果事件传递流程中的一个子view一旦获得了某个事件的处理权,那么事件的剩余操作都将由这个view来处理,直到下一次事件开始,父view才有机会再次加入到事件的处理过程中。
为了弥补这个缺陷,在Android5.0系统中,官方引入了NestedScrolling机制,NestedScrolling使得子view在滑动过程中可以先将x和y轴的滑动距离交给父view处理,父view对其处理后,再将剩余滑动距离交由子view,从而实现了滑动过程中父view和子view的信息交互。
NestedScrolling主要包括NestedScrollingParent、NestedScrollingChild两个接口和NestedScrollingParentHelper、NestedScrollingChildHelper两个工具类,我们先了解下上面的两个接口。
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();
}
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(
boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int[] offsetInWindow);
public boolean dispatchNestedPreScroll(
int dx,
int dy,
int[] consumed,
int[] offsetInWindow);
public boolean dispatchNestedFling(
float velocityX,
float velocityY,
boolean consumed);
public boolean dispatchNestedPreFling(
float velocityX,
float velocityY);
}
NestedScrollingChild接口中每个方法,NestedScrollingParent接口中大概都有相应的onXXX方法回调。
当子view接收到事件,滚动开始后,会调用startNestedScroll,通过它的返回值得知父view是否支持嵌套滚动。
如果父view支持嵌套滚动,那么就会调用dispatchNestedPreScroll,这个方法中的dx和dy参数表示本次滚动在x和y方向上的距离,并通过consumed参数数据获得父view消费的距离,consumed的值需要父view在相应的onNestedPreScroll方法中设置。
在父view完成一部分滑动距离后,子view会继续滑动,并通过dispatchNestedScroll告诉父view它在x和y方向上分别消费了的距离(dxConsumed,dyConsumed)和在x和y方向上滚动的距离中还未消费的部分(dxUnConsumed,dyUnConsumed),父view通过这些信息来决定它是否还要继续滑动。
如果有fling操作,则通过dispatchNestedFling和dispatchNestedPreFling来完成。
最后由stopNestedScroll结束整个滑动事件。
当然,除了上面的基本操作,还有一些辅助性的方法,比如onNestedScrollAccepted方法中可以执行一些初始化工作。
整个NestedScrolling机制流程如下:
子view-> startNestedScroll
父view-> onStartNestedScroll、onNestedScrollAccepted
子view-> dispatchNestedPreScroll
父view -> onNestedPreScroll
子view-> dispatchNestedScroll
父view-> onNestedScroll
子view-> stopNestedScroll
父view-> onStopNestedScroll
那么NestedScrollingParentHelper、NestedScrollingChildHelper是做什么的呢?事实上,几乎所有的工作都是由这两个工具类完成的,Helper已经实现好了子view和父view交互的具体逻辑,因此我们实现NestedScrolling机制也更加简单,只需要让父view和子view实现NestedScrollingParent和NestedScrollingChild接口,然后在接口方法中调用Helper类中相应的方法。
NestedScrollingParentHelper中的代码很简单,我们看看NestedScrollingChildHelper中的代码:
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;
}
startNestedScroll方法的主要工作就是就是寻找嵌套滚动的父view,如果找到就返回true,表示view使用嵌套滚动机制;如果没有找到就返回false,表示禁用嵌套机制。
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;
}
dispatchNestedPreScroll会先调用getLocationInWindow获取自己在父view中的位置,然后通过ViewParentCompat调用onNestedPreScroll,ViewParentCompat是一个兼容类,用来和父view进行交互,它最终会调用到父view的onNestedPreScroll方法从而让父view根据自身的逻辑进行滚动,然后子view再次调用getLocationInWindow方法获取自己的位置,并计算出偏移量,最后判断父view是否消费了距离,如果消费了,这个方法返回true,否则返回false。
没有NestedScrolling机制,我们仍然可以通过自定义view来实现类似的滑动效果,但是NestedScrolling机制的出现,给了我们一个统一的标准,它使得我们代码之间的耦合性更低,而且更易于扩展。