什么是嵌套滾動(dòng)?(如下圖):

- 一般情況下,如果我們界面有多個(gè)布局: 包括可滾動(dòng)布局(ScrollView、ListView、RecyclerView等)和不可滾動(dòng)布局(普通的View). 當(dāng)我們滾動(dòng)該滾動(dòng)布局時(shí),該布局內(nèi)會(huì)做相應(yīng)的滾動(dòng),其他的不可滾動(dòng)布局并不會(huì)有相關(guān)的變動(dòng)。這是因?yàn)椋簼L動(dòng)View在處理Touch事件時(shí),攔截了該Touch事件進(jìn)行處理,那么布局內(nèi)后續(xù)的Touch事件都不會(huì)交給其他布局(父布局/同級(jí)View),會(huì)一直下發(fā)到這個(gè)滾動(dòng)View。
- 但是,我們從
嵌套滾動(dòng)可以看出: 當(dāng)滾動(dòng)ListView時(shí),Toolbar、Bottombar、FloatingActionBar會(huì)先隱藏后,再執(zhí)行ListView的滾動(dòng)(或者是ListView滾動(dòng)過(guò)程中隱藏其他不可滾動(dòng)View)。顯然這是因?yàn)椋?滾動(dòng)View布局內(nèi)發(fā)生Touch事件時(shí),滾動(dòng)View先不處理這個(gè)Touch事件,先把它交給本身的父布局,父布局再把這個(gè)Touch事件交給與滾動(dòng)View同級(jí)的非滾動(dòng)View去處理(隱藏/改變顏色等等),等到其他View處理完成后,父布局不再需要滾動(dòng)View內(nèi)的Touch事件時(shí),滾動(dòng)View就自己去處理剩下的Touch事件。
如何實(shí)現(xiàn)嵌套滾動(dòng)
實(shí)現(xiàn)嵌套滾動(dòng)機(jī)制主要依賴四個(gè)類(lèi):
1. NestedScrollingChild //滾動(dòng)列表需要實(shí)現(xiàn)NestedScrollingChild接口,以支持將滾動(dòng)事件分發(fā)給父ViewGroup
2. NestedScrollingParent //相應(yīng)的,父ViewGroup需要實(shí)現(xiàn)NestedScrollingParent接口,以支持將滾動(dòng)事件進(jìn)一步的分發(fā)給各個(gè)子View
3. NestedScrollingChildHelper //進(jìn)行嵌套滾動(dòng)的輔助類(lèi)
4. NestedScrollingParentHelper //進(jìn)行嵌套滾動(dòng)的輔助類(lèi)
一般實(shí)現(xiàn)NestedScrollingChild接口的滾動(dòng)列表會(huì)把滾動(dòng)事件委托給NestedScrollingChildHelper輔助類(lèi)來(lái)處理。例如:RecyclerView實(shí)現(xiàn)了NestedScrollingChild接口,它內(nèi)部就會(huì)把滾動(dòng)相關(guān)事件委托給NestedScrollingChildHelper對(duì)象來(lái)處理,如下所示:
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
//NestedScrollingChild的方法有很多,更多的可參見(jiàn)源碼
//...
當(dāng)我們滾動(dòng)RecyclerView時(shí),RecyclerView首先會(huì)通過(guò)startNestedScroll方法通知父ViewGroup(“我馬上要滾動(dòng)了,是否有兄弟節(jié)點(diǎn)要一起滾動(dòng)?”),父ViewGroup會(huì)進(jìn)一步把滾動(dòng)事件分發(fā)給所有子View(實(shí)際是分發(fā)給和子View綁定的Behavior),感興趣的子View會(huì)特別關(guān)注,即Behavior.onStartNestedScroll方法返回true。
1. RecyclerView會(huì)在Down事件時(shí)調(diào)用startNestedScroll方法
我們看下NestedScrollingChildHelper.startNestedScroll方法的實(shí)現(xiàn):
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//該循環(huán)主要是尋找到能夠協(xié)調(diào)處理滾動(dòng)事件的父View,即實(shí)現(xiàn)NestedScrollingParent接口的父ViewGroup
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
//記錄協(xié)調(diào)處理滾動(dòng)事件的父View
mNestedScrollingParent = p;
//ViewParentCompat是一個(gè)和父ViewGroup交互的兼容類(lèi),如果在Android5.0以上,就用View自帶的方法,否則若實(shí)現(xiàn)了NestedScrollingParent接口,則調(diào)用接口方法。
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
上述方法會(huì)找到能夠協(xié)調(diào)處理滾動(dòng)事件的父ViewGroup,然后調(diào)用它的onStartNestedScroll方法
2. 調(diào)用父ViewGroup的onStartNestedScroll方法
因?yàn)镃oordinatorLayout實(shí)現(xiàn)了NestedScrollingParent接口,所以我們看下CoordinatorLayout.onStartNestedScroll方法:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
//詢問(wèn)每一個(gè)子View是否對(duì)滾動(dòng)列表的滾動(dòng)事件感興趣?
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//獲取和子View綁定的Behavior
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
handled |= accepted;
//做一下標(biāo)注,作為判斷后續(xù)是否接收滾動(dòng)事件的標(biāo)記
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
上述方法會(huì)遍歷每一個(gè)子View,詢問(wèn)它們是否對(duì)滾動(dòng)列表的滾動(dòng)事件感興趣,若Behavior.onStartNestedScroll方法返回true,則表示感興趣,那么滾動(dòng)列表后續(xù)的滾動(dòng)事件都會(huì)分發(fā)到該子View的Behavior。
因此,我們可以在自定義的Behavior.onStartNestedScroll方法中根據(jù)實(shí)際情況決定是否對(duì)滾動(dòng)事件感興趣。
假設(shè)CoordinatorLayout的某個(gè)子View對(duì)RecyclerView的滾動(dòng)事件感興趣(Behavior.onStartNestedScroll方法返回true)
-> CoordinatorLayout.onStartNestedScroll返回true
-> RecyclerView.startNestedScroll返回true
-> RecyclerView就會(huì)把用戶的滾動(dòng)事件源源不斷的分發(fā)給之前找到的父ViewGroup
-> 父ViewGroup則進(jìn)一步分發(fā)給感興趣的子View
-> 感興趣的子View處理完滾動(dòng)事件后,若用戶的滾動(dòng)距離沒(méi)有被消費(fèi)完
-> RecyclerView才有機(jī)會(huì)處理滾動(dòng)事件(例如:用戶一次性滾動(dòng)了10px,其中某個(gè)View消費(fèi)了8px,那么RecyclerView就只能滾動(dòng)2px了)
3.RecyclerView會(huì)在Move事件時(shí)進(jìn)行事件分發(fā)(先交給父布局,再自己處理)
@Override
public boolean onTouchEvent(MotionEvent e) {
final int action = e.getActionMasked();
...
switch (action) {
//other case...
case MotionEvent.ACTION_MOVE: {
//1.先算出滾動(dòng)距離...
//2.事件分發(fā)給父ViewGroup處理
// dispatchNestedPreScroll返回true,說(shuō)明 父ViewGroup消耗了一定距離,消耗掉的距離存儲(chǔ)在mScrollConsumed,滾動(dòng)的距離要減去父ViewGroup消耗的距離
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];
}
//3.計(jì)算出本身處理的距離...
//4.RecyclerView本身處理這些滾動(dòng)事件(scrollByInternal)
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);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}break;
//other case...
}
}
3.1 調(diào)用RecyclerView的dispatchNestedPreScroll把事件分發(fā)給父ViewGroup處理:ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed)
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
//NestedScrollingChildHelper.dispatchNestedPreScroll方法的實(shí)現(xiàn)
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
//判斷之前是否找到協(xié)同處理的父ViewGroup
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//dx和dy分別表示X和Y軸上的滾動(dòng)距離
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
//offsetInWindow用于計(jì)算滾動(dòng)前后,滾動(dòng)列表本身的偏移量
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;
//分發(fā)給父ViewGroup
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
//計(jì)算出滾動(dòng)列表本身的偏移量
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;
}
方法的第3個(gè)參數(shù)是一個(gè)長(zhǎng)度為2的一維數(shù)組,用于記錄父ViewGroup(其實(shí)是父ViewGroup的子View)消費(fèi)的滾動(dòng)長(zhǎng)度,若滾動(dòng)距離沒(méi)有用完,則滾動(dòng)列表處理剩下的滾動(dòng)距離;第4個(gè)參數(shù)也是一個(gè)長(zhǎng)度為2的一維數(shù)組,用于記錄滾動(dòng)列表本身的偏移量,該參數(shù)用于修復(fù)用戶Touch事件的坐標(biāo),以保證下一次滾動(dòng)距離的正確性。
3.2 父ViewGroup就會(huì)把滾動(dòng)事件分發(fā)給感興趣的子View
//ViewParentCompat.java
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
IMPL.onNestedPreScroll(parent, target, dx, dy, consumed);
}
}
CoordinatorLayout實(shí)現(xiàn)了NestedScrollingParent接口,所以我們看下CoordinatorLayout.onNestedPreScroll方法:
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams)view.getLayoutParams();
//若子View對(duì)滾動(dòng)事件不感興趣,則直接跳過(guò)
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
//分發(fā)給每個(gè)子View的Behavior處理
viewBehavior.onNestedPreScroll(this, view,target, dx, dy, mTempIntPair);
//找出每個(gè)子View消費(fèi)的最大滾動(dòng)距離就是父ViewGroup消費(fèi)的滾動(dòng)距離
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0]): Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1]): Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
//記錄父ViewGroup消費(fèi)的滾動(dòng)距離
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
//處理子View之間的依賴關(guān)系
dispatchOnDependentViewChanged(true);
}
}
CoordinatorLayout的處理很簡(jiǎn)單,把滾動(dòng)事件分發(fā)給各個(gè)子View的Behavior.onNestedPreScroll方法處理,并計(jì)算出最終消費(fèi)的滾動(dòng)距離。
因此,我們可以在RecyclerView滾動(dòng)之前,重寫(xiě)
Behavior.onNestedPreScroll方法中處理CoordinatorLayout的子View的滾動(dòng)事件,然后根據(jù)實(shí)際情況填寫(xiě)消費(fèi)的滾動(dòng)距離。
3.3 RecyclerView調(diào)用scrollByInternal事件分發(fā)給自己處理
假設(shè)RecyclerView的滾動(dòng)距離沒(méi)有被CoordinatorLayout消費(fèi)完,那么接下來(lái)RecyclerView應(yīng)該處理這些滾動(dòng)事件了。在RecyclerView的onTouchEvent方法中會(huì)調(diào)用scrollByInternal處理內(nèi)容滾動(dòng),關(guān)鍵代碼如下所示:
//x表示X軸上剩余的滾動(dòng)距離
if (x != 0) {
//交給具體的LayoutManager處理滾動(dòng)事件,并且記錄下消費(fèi)的和剩余的滾動(dòng)量
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler,mState);
unconsumedX = x - consumedX;
}
//y表示Y軸上剩余的滾動(dòng)距離
if (y != 0) {
//交給具體的LayoutManager處理滾動(dòng)事件,并且記錄下消費(fèi)的和剩余的滾動(dòng)量
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
//...
//分發(fā)滾動(dòng)列表本身對(duì)剩余滾動(dòng)量的消費(fèi)情況
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset);
如上所示,RecyclerView通過(guò)LayoutManager處理了剩余的滾動(dòng)距離,如果onNestedPreScroll之后的剩余滾動(dòng)量沒(méi)有被RecyclerView消耗完,又可以分發(fā)給父ViewGroup,父ViewGroup再分發(fā)給感興趣的子View的Behavior處理。這部分的代碼邏輯和onNestedPreScroll類(lèi)似,就不貼出了,感興趣的可以直接看源碼。
因此,我們可以在RecyclerView滾動(dòng)時(shí)或滾動(dòng)后,重寫(xiě)
Behavior.onNestedScroll方法處理CoordinatorLayout的子View的滾動(dòng)事件,去消耗RecyclerView的滾動(dòng)量
4. RecyclerView會(huì)在UP事件時(shí)stopNestedScroll
假設(shè)用戶結(jié)束滾動(dòng)操作了,即應(yīng)該結(jié)束一系列的滾動(dòng)事件了,RecyclerView會(huì)在UP事件中調(diào)用stopNestedScroll方法,該方法和上面介紹的三個(gè)方法類(lèi)似,都會(huì)先把事件分發(fā)給父ViewGroup,然后父ViewGroup再把事件分到各個(gè)子View,最終觸發(fā)子View的Behavior.onStopNestedScroll方法,感興趣可以可接看源碼,此處不再貼出。
因此,我們可以在自定義的Behavior.onStopNestedScroll方法中檢測(cè)到滾動(dòng)事件的結(jié)束。
總結(jié):
整個(gè)嵌套滾動(dòng)機(jī)制就介紹完了,可見(jiàn)跟我們直接打交道的就是
CoordinatorLayout.Behavior類(lèi)了,通過(guò)重寫(xiě)該類(lèi)中的方法,我們不僅可以監(jiān)聽(tīng)滾動(dòng)列表的滾動(dòng)事件,還可以做很多其他的事情。
下一篇會(huì)重點(diǎn)介紹:CoordinatorLayout.Behavior
摘抄總結(jié)自: Android CoordinatorLayout和Behavior