View中NestScroll方法如图所示。所谓嵌套滚动,是针对View与ViewGroup来说的,即二者联动。
当我们手指在一个View上滑动一定delta距离时,通过scrollTo让View实现delta距离的滚动,触屏事件完全消耗在View上面,父View无法消费滑动事件。为了让View与父View同时对某一滑屏距离做出滑动反应,View和ViewGroup的交互接口ViewParent包含NestedScroll相关方法,于是就有了嵌套滚动这个东西。
一种是父View主动型,子View在消费事件滑动的距离时,先问问父View,父View主动决定自己消耗掉多少距离,然后给子View留多少距离。
另一种是子View主动型,子View先消耗,然后将消耗与未消耗的距离一起告诉父View。
注意一点,嵌套滚动父视图的方法都是onXxx开头的,有的视图既可以在嵌套滚动中作为子视图,也能作为父视图。
查找可接受嵌套滚动的父视图
View开始准备NestScroll嵌套滚动时,触发View#startNestedScroll方法。
View#startNestedScroll方法。
public boolean startNestedScroll(int axes) {
//已经存在mNestedScrollingParent的节点,直接返回
if (hasNestedScrollingParent()) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
- hasNestedScrollingParent方法,View内部的mNestedScrollingParent,如果已经初始化过,直接返回true。否则,需要向上找到一个配合嵌套滚动的父视图。
- View支持嵌套滚动时,isNestedScrollingEnabled方法是true,如果一个View支持嵌套滚动,提前通过setNestedScrollingEnabled方法设置mPrivateFlags3标志位。
- 在树视图结构中,向上一层层查找ViewParent父节点,调用ViewParent#onStartNestedScroll方法,父视图支持嵌套滚动的条件是onStartNestedScroll方法返回true,ViewGroup#onStartNestedScroll默认返回值false。因此,支持NestScroll的父容器需重写onStartNestedScroll方法。
- 如果没有父视图的onStartNestedScroll方法是true,说明均不支持嵌套滚动。
ViewGroup#onStartNestedScroll方法
@Override
public boolean onStartNestedScroll(View child, View target,
int nestedScrollAxes) {
return false;
}
- 重写ViewGroup#onStartNestedScroll方法,父容器接受嵌套滚动,将View的mNestedScrollingParent设为该父容器。
- ViewGroup#onNestedScrollAccepted方法设置axes滚动轴。
父视图的onStartNestedScroll方法需要重写,返回true,才会支持配合子视图嵌套滚动,同时,赋值子视图内部mNestedScrollingParent。
通过startNestedScroll验证同步View与父View支持NestedScroll后,下面开启滚动。
子视图dispatchNestedPreScroll方法
View#dispatchNestedPreScroll,把自己的滚动机会先让给父View
可以在子视图触摸事件中调用,该方法在基类View。
View#dispatchNestedPreScroll方法。
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;
...
//先初始化为0,
consumed[0] = 0;
consumed[1] = 0;
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
...
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
父View触发onNestedPreScroll方法时,通过父View的scrollBy方法移动父View的内部视图,将移动的(即消耗掉)的距离存储在consumed中,通过consumed数组通知子View,我已经消耗掉你应该移动的距离了,你自己就别动了,跟我一起动吧,这时处理的是子View的onTouch事件。
父View先消耗,子View紧跟着父View移动,未消耗掉的子View继续消耗,让子View与父View联动。
父View重写onNestedPreScroll方法。ViewGroup#onNestedPreScroll方法默认什么都不做,最近发现23版本的ViewGroup在该方法什么都没做,而27版本的默认会触发dispatchNestedPreScroll方法,意思就是,如果父视图也是可以嵌套滚动,去找他自己的父视图去消费哈,如果上层没有支持的父视图,也什么都不做了。
子视图dispatchNestedScroll方法
View#dispatchNestedScroll,把自己的滚动机会先让给自己。
View#dispatchNestedScroll。
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;
...
mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
...
return true;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
父View触发onNestedScroll方法时,子View主动将消费的距离与未消费的距离通知父View。
父View重写onNestedScroll方法。ViewGroup#onNestedScroll方法默认什么都不做。
当父View收到子View未能消费的距离后,在父View的坐标下完成剩余偏移。这种场景是子View主动先消费,剩下的交给父View。
总结一下
注意ViewGroup的onNestedScroll方法和onNestedPreScroll方法,他们都是去消费子视图的移动距离。
dispatchNestedPreScroll和dispatchNestedScroll的区别,前者把机会先让给父视图,后者把机会先留给自己。
任重而道远