前言
1、类概述:
NestedScrollingChild接口
定义了“与父view交互”的方法,需要嵌套滑动的子view就实现它;
NestedScrollingChildHelper帮助类
协助子view(如RecyclerView)实现“与父view交互”的方法。在实现这些方法时,发现有很多逻辑是可以共用的,所以抽了一个Helper类,专门来实现这些逻辑,让其它需要嵌套滑动的子view可以复用,也让RecyclerView的代码更简介、易懂;
NestedScrollingParent接口
定义了“与子view交互”的方法,需要嵌套滑动的父view就实现它;
NestedScrollingParentHelper帮助类
功能跟上面的xxxChildHelper一样,但是由于不同父view与子view交互差别很大,所以这个帮助类里并没有多少功能,具体实现还是在父view里自己实现;
2、嵌套滑动流程概述:
子view接收touch事件,开始滑动前,通过ViewParentCompat这个类通知父view,父view做相应处理;
子view滑动后,通知父view自己的滑动情况,父view做相应处理;
滑动机制源码分析
NestedScrollingChild和NestedScrollingParent中的方法都是一一对应的,子view通过某个方法通知父view,父view通过某个方法接收到通知,对应关系如下:
启动嵌套滑动:startNestedScroll() —— onStartNestedScroll();
滑动前:dispatchNestedPreScroll() —— onNestedPreScroll();
滑动:dispatchNestedScroll() —— onNestedScroll();
fling滑动前:dispatchNestedPreFling() —— onNestedPreFling();
fling滑动:dispatchNestedFling() —— onNestedFling();
停止滑动:stopNestedScroll() —— onStopNestedScroll();
下面就用RecyclerView、NestedScrollView的源码来分析(由于NestedScrollingParentHelper非常简单,也没做什么实际实现,下面的源码分析中就不贴出了)。
假设子view是RecyclerView,父view是NestedScrollView,父父view是也是NestedScrollView(后面简称爷view),总共三层。
启动嵌套滑动
RecyclerView源码:
// 在dispatchTouchEvent、onTouchEvent方法中,都会调用startNestedScroll()
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;
}
// 如果canScroll,就启动嵌套滑动,通知父view。至于如何通知,见下面源码
startNestedScroll(nestedScrollAxis);
} break;
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) {
// 通知父view,如果父view接受嵌套滑动,就调用其onNestedScrollAccepted()方法
// 如果父view不接受嵌套滑动,就继续通知爷view......直到找到一个接受的
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
// 如果找到一个接受嵌套滑动的父view,就直接返回了,不再继续通知爷view了
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
NestedScrollView源码:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// 如果是纵向滑动,就接受,返回true;否则就返回false
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
// 记录嵌套滑动的方向
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
// 父view接受了嵌套滑动之后,子view就直接返回,不再继续通知爷view了,所以通知爷view就由父view来完成;
// 父view也继承了NestedScrollingChild接口,并和子view一样使用同样的helper,通知爷view的方式也是一样
}
滑动前
RecyclerView源码:
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;
// 通知父view,把滑动情况通知给父view。如果父view处理了,就减去父view消费的距离,剩下的就是子view需要滑动的距离
// 把实参mScrollConsumed传过去,父、爷view消费的距离会记录在上面
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
......
......
......
NestedScrollingChildHelper源码:
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;
// 通知父view执行onNestedPreScroll()方法
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;
}
NestedScrollView源码:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 跟子view一样,把滑动情况通知给爷view
dispatchNestedPreScroll(dx, dy, consumed, null);
}
滑动后
RecyclerView源码:
// 该方法在dispatchTouchEvent、onTouchEvent中调用,调用时已完成滑动。参数x、y分别是手指滑动的距离
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
// 消费的距离就是子view真实滑动的距离
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();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
// 调用dispatchNestedScroll()通知父view本次滑动情况
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
// mScrollOffset记录的是,因为父view的滑动,导致子view的偏移。通过这个偏移量,重新计算当前手指的位置,保证下次滑动的准确性
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) {
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;
}
NestedScrollingChildHelper源码:
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;
if (offsetInWindow != null) {
// 父view滑动前,子view的位置
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
// 通知父view调用onNestedScroll()方法
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
// 父view滑动后,子view的位置
mView.getLocationInWindow(offsetInWindow);
// 父view滑动后的位置减去之前的位置,得到偏移量
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;
}
NestedScrollView源码:
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int oldScrollY = getScrollY();
// 滑动dyUnconsumed
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
final int myUnconsumed = dyUnconsumed - myConsumed;
// 通知爷view滑动情况。由于把子view未消费的dy全部消费掉了,所以爷view在dy上不会有滑动了,也就自然不需要传实参偏移量了
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
fling滑动前
RecyclerView源码:
// 在onTouchEvent方法的Action.UP中,会根据当前手指离开屏幕的方向、速度等,计算出最终可以滑动的距离,然后调用fling()方法,下面的代码是fling()方法中的一段
// fling滑动前,通知父view,如果父view接受fling滑动,子view将不再处理fling滑动;否则就自己fling滑动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
NestedScrollingChildHelper源码:
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
// 通知父view调用onNestedPreFling()方法
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
velocityY);
}
return false;
}
NestedScrollView源码:
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
// 继续通知爷view
return dispatchNestedPreFling(velocityX, velocityY);
}
fling滑动后
RecyclerView源码:
// 跟fling滑动前是同一段代码,就不做太多解释
// 如果父view、爷view都没有拦截,就通知父view,并执行fling滑动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
// 如果自己能滑动,就自己fling,并通知父view
dispatchNestedFling(velocityX, velocityY, canScroll);
// 如果mOnFlingListener为null,或正在fling中,就返回
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
// 开始fling滑动
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
NestedScrollingChildHelper源码:
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
// 通知父view调用onNestedFling()方法
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
velocityY, consumed);
}
return false;
}
NestedScrollView源码:
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
// 如果子view没有滑动,就自己滑动
if (!consumed) {
flingWithNestedDispatch((int) velocityY);
return true;
}
return false;
}
// 这里有2个值得注意的点:
// 1、fling传递的参数不再是消费多少、未消费多少,而是只有一个速度,说明google其实并不推荐两个控件同时fling;
// 2、父view执行完fling后,也没有向爷view通知,可能也是不希望让爷view再对fling进行处理;
停止滑动
RecyclerView源码:
// dispatchTouchEvent中,在ACTION_UP和ACTION_CANCEL中调用了stopNestedScroll()方法
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
stopNestedScroll();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
}
NestedScrollingChildHelper源码:
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
// 通知父view调用onStopNestedScroll()方法
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
NestedScrollView源码:
@Override
public void onStopNestedScroll(View target) {
// 停止滑动,并通知爷view
mParentHelper.onStopNestedScroll(target);
stopNestedScroll();
}