RecyclerView中的NestedScroll流程
-
手指触碰之后,
onInterceptTouchEvent
触发ACTION_DOWN
:- 调用
startNestedScroll
,沿着View树往上寻找可以接受嵌套滑动的父View,如果找到了,则会回调父View的onStartNestedScroll
以及onNestedScrollAccepted
- 调用
-
当手指滑动的时候,触发
onTouchEvent
中的ACTION_MOVE
:- 调用
dispatchNestedPreScroll
将嵌套滑动事件给父View,询问父View需要消费多少距离,其中就会回调父View的onNestedPreScroll
- 接着调用
dispatchNestedScroll
将已经消费的距离与未消费的距离回调给父View,是否父View要对当前的View进行移动
- 调用
-
当手指离开屏幕时,触发
onInterceptTouchEvent
触发ACTION_UP
:- 调用
stopNestedScroll
将停止事件告诉父View
- 调用
NestedScroll的实现
- 当手指触摸到RecyclerView时,根据Touch事件的传递,会触发
onInterceptTouchEvent
判断是否要中断事件传递。- 在
ACTION_DOWN
分支中,会初始化Touch的X,Y位置,并且判断当前RecyclerView是允许横向或者纵向滑动,最后将滑动标志位以及滑动类型交给startNestedScroll
- 在
ACTION_UP
分支中,会调用stopNestedScroll
停止嵌套滑动
- 在
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
...
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
...
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
...
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
case MotionEvent.ACTION_UP: {
...
stopNestedScroll(TYPE_TOUCH);
} break;
...
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
PS:其中省略部分与嵌套滑动无关的代码
- 在
startNestedScroll
中,调用NestedScrollingChildHelper
的startNestedScroll
函数-
hasNestedScrollingParent
:判断当前是否有正在进行嵌套滑动的父View,如果有的话说明当前正处于滑动状态,直接返回不用处理 - 判断
isNestedScrollingEnabled
嵌套滑动是否可用,如果不可用则直接返回false - 递归向父View调用
onStartNestedScroll
询问,是否可以开始嵌套滑动,如果允许的话,则调用setNestedScrollingParentForType
设置当前嵌套滑动的父View,并且调用onNestedScrollAccepted
,这个函数主要用来设置嵌套滑动的方向(横向/纵向)
-
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
- 当开始滑动时,
ACTION_MOVE
的分支代码中:- 计算当前与
ACTION_DOWN
之间移动的距离,也就是dx,dy
- 调用
dispatchNestedPreScroll
将嵌套滑动的事件Pre-Scrolling
分发给父View,并且判断父View需要消费(consume)多少,返回值代表父View是否有消费距离。如果有消费的话,则将dx,dy
减去消费的距离 - 如果当前的状态处于
SCROLL_STATE_DRAGGING
即正在拖动的话,则会将mLastTouchX,mLastTouchY
更新,如果不更新的话,那么下一次计算的数据会出错。 - 更新完后,会调用
scrollByInternal
开始滑动``dx,dy```的距离
- 计算当前与
@Override
public boolean onTouchEvent(MotionEvent e) {
...
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
...
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
...
}
vtev.recycle();
return true;
}
-
dispatchNestedPreScroll
调用的就是NestedScrollChildHelper.dispatchNestedPreScroll
- 首先判断嵌套滑动是否可用
- 判断
dx,dy
是否均为0,如果均为0的话,则代表没有偏移 - 如果有偏移的话,则从当前的View获取在Window中的偏移量
- 调用
ViewParent. onNestedPreScroll
函数传入当前偏移的距离dx,dy
让父View判断需要消费多少距离,通过consumed
数据传回 - 最后计算完
offsetInWindow
偏移量后返回是否父View有消费
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
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(parent, mView, dx, dy, consumed, type);
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;
}
- 在
scrollByInternal
函数中,开始进行RecyclerView的滑动- 通过
mLayout.scrollHorizontallyBy
获取LayoutManager的对于scroll的消费距离 - 调用
dispatchNestedScroll
告知父View已经消费的距离consumedX,consumedY
以及还剩余的距离unconsumedX,unconsumedY
- 当父View处理完消费的X以及Y之后,更新
mLastTouchX,mLastTouchY
- 通过
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
// 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];
} else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
- 在
dispatchNestedScroll
调用的NestedScrollChildHelper.dispatchNestedScroll
:- 判断是否支持嵌套滑动
- 回调父View的
onNestedScroll
将已经消费的距离与未消费的距离传入 - 在父View中的回调函数中可以操作RecyclerView进行移动
- 移动完后得到与移动前的偏移差,返回给
scrollByInternal
进行滑动距离的计算
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
- 最后在
onInterceptTouchEvent
中的ACTION_UP
中调用stopNestedScroll
结束整个嵌套滑动的过程
Fling的流程与Touch的流程接近,也是先询问耗费多少再在内部进行处理