上一篇文章我們介紹了NestedScrollingParent和NestedScrollingChild接口,瞭解了兩個接口裏的方法和相互之間的調用關係。這篇我們以NestedScrollView
類爲例,看先嵌套滾動Parent和Child之前具體是怎麼實現的。爲啥用NestedScrollView
呢,因爲這既是一個NestedScrollingParent又是一個NestedScrollingChild,瞭解了整個類後就瞭解了整個機制了。
這裏就不全部貼出了源碼,後面我們一邊講解一邊貼出源碼。
一、類簡介
還是老規矩,我們先看下Google對這個類的介紹
NestedScrollView is just like
ScrollView
, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.
就是說NestedScrollView
和ScrollView
類似,是一個支持滾動的控件。此外,它還同時支持作爲NestedScrollingParent
或者NestedScrollingChild
進行嵌套滾動操作。默認是啓用嵌套滾動的。
再看下繼承關係
public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
NestedScrollingChild2, ScrollingView {}
可以看到該類繼承自FrameLayout
,實現了NestedScrollingParent
、NestedScrollingChild
和
ScrollingView
接口。所以才具有上訴的特性咯。
另外這裏有個一個NestedScrollingChild2
,在上篇文章已經提到了,這個其實核心和NestedScrollingChild
是一樣的,只是在部分方法上面多了一個type字段用於判斷而已。基本上就可以直接看成NestedScrollingChild
接口。
另外這裏說明一下,因爲這裏重點是研究嵌套機制的,所以並不是所有的源碼都有涉及,只介紹與嵌套相關的
二、嵌套滾動流程分析
1、總流程介紹
總的來說Parent和Child之間的相互調用遵循下面的調用關係:
2、具體分析
NestedScrollView是一個FrameLayout也就是一個ViewGroup,根據Android的觸摸事件分發機制,一般會進入到onInterceptTouchEvent(MotionEvent ev)
進行攔截判斷。所以我們也就從這裏作爲分析的入口。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
//mIsBeingDragged標識當前View是否在移動 這裏的意思在移動或者移動事件都進行攔截
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
// 是滑動事件 並且是垂直方向
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
// 進入這裏說明是自己想處理的情況了 所以設置mIsBeingDragged 用於攔截事件
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
// 因爲自己要處理 所以叫Parent不要攔截
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
// 判斷是否是在子控件區域
if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
// 記錄按下的位置
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
// 如果是在慣性滑動中的點擊 交給自己處理
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
// 這裏就是開始嵌套滑動的地方了
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
// 停止嵌套滑動((前提是要作爲NestedScrollingChild))
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
這個方法的代碼量不大,部分說明已經寫在註釋裏了,這裏總結起來主要就是做了一下幾件事情:
在ACTION_DOWN中:一個是判斷是否需要攔截事件,二是在合適的時候調用startNestedScroll();
方法
在ACTION_MOVE中:在需要處理的情況下,將mIsBeingDragged
置爲true,將事件傳遞給自己的onTouchEvent()
方法進行處理。
在ACTION_UP或者ACTION_CANCEL中:將mIsBeingDragged
重置爲false,然後調用stopNestedScroll()
停止嵌套滑動。
這裏我們看下startNestedScroll();
和stopNestedScroll()
的實現。
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
可以看到,就是簡單代理給了ChildHelper進行處理,根據上篇文章的解析,我們知道這兩個方法的調用,最終會進入到Parent對應的onStartNestedScroll(View child, View target, int nestedScrollAxes)
和onStopNestedScroll(View target)
方法。那我們就繼續看下這兩個方法的處理吧
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// 只處理垂直滑動的情況
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
stopNestedScroll();
}
onStartNestedScroll()
方法中,就是判斷是否是垂直滑動,是的話,返回true,表示進行處理。
onStopNestedScroll(View target)
方法,代理給ParentHelper,然後繼續調用stopNestedScroll();
繼續通知它的NestedScrollingParent(如果有的情況)停止嵌套滑動。
PS:這裏有個主意的地方哦,有些人看到這裏可能會覺得這不是形成死循環了麼
stopNestedScroll()->mChildHelper.stopNestedScroll()->onStopNestedScroll()->stopNestedScroll()
,形成一個閉環了。有這個誤解的朋友是因爲沒有區分NestedScrollingParent和NestedSrollingChild身份。當我們調用
stopNestedScroll()
方法的時候,當前的NestedScrollView是必須是具有Child的有效的身份,如果是Parent這個方法沒有意義的,相當於是一個空方法。對應的
onStopNestedScroll(View target)
方法,是作爲Parent有效身份的時候,纔會被回調。對於Child身份也是沒有意義的。所以其實從stopNestedScroll()
到stopNestedScroll()
已經從一個對象(Child)到另一個對象了(Parent)的傳遞,不是在同一個對象裏的調用,所以是不會死循環的。後面不會再做說明,記得所有重寫的NestedScrollingChild接口的方法,只有在Child身份的對象上有效,所有重寫的NestedScrollingParent接口的方法,只有在Parent身份的對象上有效。
到這裏,startNestedScroll和stopNestedScroll這兩組流程分析就完了。那中間真正的分發流程在哪兒呢?那就是onTouchEvent()
方法啊,在上面onInterceptTouchEvent(MotionEvent ev)
方法裏面,適當的時候,不是做了攔截操作麼,那就會進入onTouchEvent()
方法咯,而且如果該類的子View沒有消費掉觸摸事件,正常情況也會再分發到該類的onTouchEvent()
方法。
這裏我們就繼續這個方法的分析吧
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
// 沒有Child,那還滑動個啥啊,都撐不開佈局
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
// 通知父View不要攔截觸摸事件
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
// 如果是在慣性滑動中 停止滑動
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
// 記錄按下的位置
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
// 也是開啓嵌套滑動
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
// 手指滑動距離
int deltaY = mLastMotionY - y;
// 先分發給Parent進行預處理
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
// 如果Parent消費了滑動距離 需要減去
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
// 也是告知父View不要攔截事件
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
// 滾動自己
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
// 重新計算未消費的距離
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 分發給Parent進行嵌套滾動
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
// 如果Parent沒有消費 並且可以滾動 繼續處理
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
// 如果達到了慣性的速度 分發慣性滑動事件
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
mActivePointerId = INVALID_POINTER;
// 這個方法裏面會調用stopNestedScroll(ViewCompat.TYPE_TOUCH);
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
mActivePointerId = INVALID_POINTER;
// 這個方法裏面會調用stopNestedScroll(ViewCompat.TYPE_TOUCH);
endDrag();
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
// 這裏始終返回true 所以Parent作爲父View其實是進不了onTouchEvent()方法的。
return true;
}
這個方法比較長,裏面涉及到一些具體滾動操作,這不是我們本篇的重點,所以我們看關鍵地方就可以了。重要地方我都寫了註釋說明。
先看返回值,這裏直接返回了true,從這裏知道,Parent和Child嵌套的時候,Parent是肯定進不了該方法的。所以這裏面的情況,我們只需要考慮Child身份即可。
ACTION_DOWN中:
如果是慣性滑動的情況,停止滑動。同樣是調用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
開啓嵌套滑動,所以Parent中的onStartNestedScroll()
可能不止一次調用。但是多次調用有影響嗎?沒影響,在裏面沒有做具體滾動操作,只是做是否需要處理的判斷而已。
ACTION_MOVE中:
先通過調用dispatchNestedPreScroll()
分發給Parent進行滾動處理。然後再通過overScrollByCompat()
自己處理滾動事件,最後再計算一下未消費的距離,再通過dispatchNestedScroll()
繼續給Parent進行處理。同時根據返回值,判斷Parent是否處理了,進行下一步操作。
這裏的dispatchNestedPreScroll()
,就會進入Parent的onNestedPreScroll()
的方法,我們看下處理:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
dispatchNestedPreScroll(dx, dy, consumed, null);
}
可以看到其實啥也沒做,就是繼續給它自己的Parent(如果有的情況)分發事件
接下來再看下dispatchNestedScroll()
對應的Parent的onNestedScroll()
方法
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int oldScrollY = getScrollY();
// 滾動自己 消費掉距離
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
final int myUnconsumed = dyUnconsumed - myConsumed;
// 繼續分發給上一級
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
這裏面也比較簡單,就是使用scrollBy()
方法,滾動自己。消費掉滾動距離。同樣在自己還有Parent的情況下,繼續向上分發。
到這裏,ACTION_DOWN的情況,我們就介紹完了,繼續看UP和CANCEL
ACTION_UP和ACTION_CANCEL中:
在這兩個case中,最後都調用了endDrag()。我們看下這個方法
private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
// 停止嵌套滾動
stopNestedScroll(ViewCompat.TYPE_TOUCH);
if (mEdgeGlowTop != null) {
mEdgeGlowTop.onRelease();
mEdgeGlowBottom.onRelease();
}
}
這裏面就是調用stopNestedScroll(ViewCompat.TYPE_TOUCH);
,這個方法前面已經分析了,這裏不做多的說明。
回到ACTION_UP中。這裏面在調用endDrag()
之前,還調用了flingWithNestedDispatch()
方法,看下具體實現:
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0)
&& (scrollY < getScrollRange() || velocityY < 0);
// 先給Parent看是否需要處理
if (!dispatchNestedPreFling(0, velocityY)) {
// 再次回調Parent,其實主要目的通過canFling參數,是告訴Parent我自己處理了
dispatchNestedFling(0, velocityY, canFling);
// 沒有處理 自己處理
fling(velocityY);
}
}
這裏面就是繼續慣性滑動事件的分發而已,註釋說的很清楚了,就不解釋了。那我們就繼續看Parent中對應的兩個方法:onNestedPreScroll()和onNestedFling()
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
flingWithNestedDispatch((int) velocityY);
return true;
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
處理很簡單,onNestedPreFling()
直接往上一級Parent分發,onNestedFling()
直接,看Child是否消費了,沒有消費往上一級Parent分發。並返回true,如果已經消費了,直接返回fasle即可。
到這裏,整個流程就分析完了,還是做一個簡單的總結吧。
3、總結
大家再通過這張流程圖(畫的不好,將就看了)自己回憶和梳理一下吧。