综述
NestedScrolling 主要用于嵌套滑动。
主要思路:子 View 收到滑动事件后,先交给父 view 进行消费,父 view 消费后剩余的距离才是子 view 自己消费的距离。
在高版本 View 中默认都实现了嵌套滑动相关的方法。下面以 RecyclerView 源码进行分析。为描述方便,将 RecyclerView 称为 child、子 View 等。
startNestedScroll 与 onStartNestedScroll
前者由子 View 调用询问父 View 是否要嵌套滑动,参数为滑动方向(垂直、水平或垂直+水平),后者是父 View 中对应的回调。
RecyclerView#onTouch 在处理 ACTION_DOWN 时会调用 startNestedScroll()。
startNestedScroll() 的实现在 View.java 中,它的主要思路是依次遍历祖先 View,直到 onStartNestedScroll() 返回 true,或者本次不需要嵌套滑动
关于 onStartNestedScroll() 的实现可参考 CoordinatorLayout 实现。
// View.java
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) { // while 循环,一直往上遍历
try {
// 调用父 View 对应的 onStartNestedScroll
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
// 如果父 View 愿意嵌套滑动,给父 View 一直初始化嵌套滑动初始条件的机会
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
dispatchNestedPreScroll 与 onNestedPreScroll
前者由子 View 调用,在发生滑动时将滑动距离等提交给父 View,由父 View 先享用,它的实现在 View 中。后者是前者的对应的回调,具体实现可参考 CoordinatorLayout。
dispatchNestedPreScroll() 的参数依次为:水平、垂直滑动距离,数组(父 View 将自己消费掉的距离填入),数组(MotionEvent 坐标值的修正值)。
解释下修正值。因为父 View 可能存在滑动,导致子 View 的位置发生变化,因此需要对触摸点的坐标进行修正,保证触摸点与子 View 相对位置不变。
dispatchNestedPreScroll() 的主要逻辑在 RecyclerView#onTouchEvent() 中对于 MOTION_MOVE 的处理部分
// RecyclerView#onTouchEvent() 方法中 MOTION_MOVE 分支节选
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
// 调用 dispatchNestedPreScroll(),由父 View 先享用
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
// 父 View 享用完了,子 View 需要除掉父 View 享用的部分
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
// vtev 是 MotionEvent 类型。此处就是对 MotionEvent 进行修正
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
// 省略
}
// 上个 if 中如果的确是滑动(手指移动距离足够大),这个判断就一定成立
if (mScrollState == SCROLL_STATE_DRAGGING) {
// 记录 lastX, lastY。也会进行修正
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 通过 scrollBy 进行移动
// 【关键点一】
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
上面是 dispatchNestedPreScroll() 的具体使用场景,下面看具体实现
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
// mNestedScrollingParent 就是在上面 startNestedScroll() 中进行的赋值
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
// 先记录父 View 在滑动前子 View 的位置
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
// 对 consumed 进行补偿,防止出现 null
// 调用父 View 的 onNestedPreScroll(),用于父 View 执行真正的滑动
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
if (offsetInWindow != null) {
// 父 View 滑动后再次计算子 View 位置
getLocationInWindow(offsetInWindow);
// 用 offsetInWindow 记录父 View 滑动导致子 View 的移动量
// 用于对 MotionEvent 修正。具体逻辑见上面 MOTION_MOVE 处理
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;
}
dispatchNestedScroll 与 onNestedScroll
经过上面两步后,子 View 有可能未完全消费剩余的滑动距离,因此它需要将未消费的部分再次提交给父 View,由父 View 进行处理。这两个方法就是用来实现该功能的。
在上面的【关键点一】处调用了 scrollByInternal(),它是这两个方法的入口
// RecyclerView.java
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
if (mAdapter != null) {
// RecyclerView 将滑动处理交给 LayoutManager 处理
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
}
// 调用 dispatchNestedScroll()
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// 与 pre 阶段类似,由于父 View 的移动需要对 MotionEvent 进行修正
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
// 如果父 View 也不愿意处理剩余的距离,要么 RecyclerView 显示滑到头后的逻辑,要么就浪费掉
else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
return consumedX != 0 || consumedY != 0;
}
dispatchNestedScroll() 与 pre 阶段的逻辑完全类似,它会调用父 View 的 onNestedScroll(),将剩余处理提交给父 View。过程略
总结
整个 NestedScrolling 流程已经学习完毕(除了 fling 阶段)。主要思路为:
- DOWN 事件时子 View 先询问父 View 要不要嵌套滑动。如果不要,整个流程结束;如果要,继续执行
- MOVE 事件时子 View 会将滑动距离先提交给父 View 处理。代码表现是
pre 阶段 - 对于【2】中剩余的距离由子 View 自己处理
- 对于【3】中剩余距离会再次提交给父 View,看父 View 是否要处理。如果不需要,则自己处理或直接丢掉。