前言
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();
}