??本來認(rèn)為自己對嵌套滑動的理解和應(yīng)用還是不錯的舱沧,但是最近做了一個跟手動畫的需求组题,使用嵌套滑動發(fā)現(xiàn)了這里有了很多的坑芯急,本文來根據(jù)自身的踩坑經(jīng)歷和經(jīng)驗來總結(jié)使用嵌套滑動的注意項谬以。
??本文不會介紹嵌套滑動的基本使用,不了解的同學(xué)可以參考我的文章:Android 源碼分析 - 嵌套滑動機制的實現(xiàn)原理雹有。同時,本文嵌套滑動皆以RecyclerView
為例臼寄。
1. 不要在onInterceptTouchEvent方法里面攔截事件
??如果你有一個ViewGroup作為RecyclerView
的父布局霸奕,這個ViewGroup主要來處理一些嵌套滑動的邏輯,比如說使用系統(tǒng)的SwipeRefreshLayout
來做下拉刷新吉拳。如果這個ViewGroup
不可能有父布局處理嵌套滑動质帅,那么是否重寫onInterceptTouchEvent
可以自身需求來定,比如說SwipeRefreshLayout
就重寫了留攒。
??但是如果你的業(yè)務(wù)場景可能還會有ViewGroup來處理嵌套滑動煤惩,作為關(guān)系鏈中間的View千萬不要重寫onInterceptTouchEvent
。
??可能有對此有疑惑炼邀,現(xiàn)在我以一個具體的場景來解釋具體的原因魄揉,假設(shè)有如下一個場景:
??整個事件傳遞的流程是:首先由
RecyclerView
產(chǎn)生嵌套滑動的事件,然后提交給SwipeRefreshLayout
嘗試著處理汤善, SwipeRefreshLayout
收到事件之后什猖,發(fā)現(xiàn)還有父View可能會處理,然后在提交給ViewGroup红淡,ViewGroup
根據(jù)自身條件選擇消費一定的距離不狮,然后又返回給SwipeRefreshLayout
,SwipeRefreshLayout
在根據(jù)自身條件選擇消費在旱,最后RecyclerView
在消費摇零。整個事件傳遞和消費的流程如下:??這里存在一種特殊情況,如果中間的
SwipeRefreshLayout
重寫了onInterceptTouchEvent
方法桶蝎,導(dǎo)致事件不能傳遞到RecyclerView
驻仅,從而導(dǎo)致了嵌套滑動的機制不能觸發(fā)。有人可能有人疑問: SwipeRefreshLayout
自己想攔截事件登渣,并且處理事件噪服,這難道有問題嗎?
??針對這個問題胜茧,我想說的是粘优,正常情況下是沒有問題的仇味,但是如果ViewGroup
必須跟手變化,只有ViewGroup
跟手變化到最終態(tài)才能讓 SwipeRefreshLayout
下拉或者RecyclerView
滑動雹顺,這種情況下丹墨,不走嵌套滑動的邏輯根本沒法實現(xiàn)。
??可能有人會提出相應(yīng)的解決方法:我重寫ViewGroup
的onInterceptTouchEvent
方法來攔截事件嬉愧,然后消費事件不行嗎贩挣?針對于這種解決方法,我想問的是没酣,如果一次滑動產(chǎn)生10px的有效距離王财,而ViewGroup
只能消費其中的5px,剩下的5px怎么辦呢四康?根據(jù)情況傳遞到子View中去或者不消費搪搏?首先不消費是肯定不行的,否則就會顯得滑動不靈敏闪金,其次如果傳遞到子View中去疯溺,這也太麻煩了嘛。
??像這種情況哎垦,我們最好的解決方法就是所有的滑動走嵌套滑動的邏輯囱嫩,因為嵌套滑動本身自己支持消費部分距離的功能,而不用我們?nèi)ヌ厥馓幚怼?br>
??解釋了在什么情況下不要重寫onInterceptTouchEvent
方法之后漏设,我們現(xiàn)在來解釋一下系統(tǒng)的SwipeRefreshLayout
為什么要重寫onInterceptTouchEvent
墨闲。
- Google爸爸默認(rèn)為
SwipeRefreshLayout
已經(jīng)嵌套滑動關(guān)系鏈上最后一個View了,SwipeRefreshLayout
不可能再有父View處理嵌套滑動郑口。- 重寫
onInterceptTouchEvent
可以為SwipeRefreshLayout
增加一個新特性--就是不用依賴子View就可以實現(xiàn)下拉刷新鸳碧。也是說,我們在xml布局中直接添加一個SwipeRefreshLayout
犬性,不用給它添加子View就能下拉刷新瞻离。這也是嵌套滑動的弊端,必須得有一個View來產(chǎn)生嵌套滑動乒裆。
??針對于上面兩個原因套利,還是不能說服我堅持的觀點--在嵌套滑動鏈上的View不用重寫onInterceptTouchEvent
方法。為什么呢鹤耍?上面的第二個問題肉迫,我們還是可以避免:既然是鏈上最底端的View,可以完全自己產(chǎn)生嵌套滑動事件稿黄,然后嘗試著傳遞到父View喊衫,然后自己在消費,而不用去攔截事件杆怕。這樣的話族购,整個關(guān)系鏈都不會破壞鼻听。所以我對系統(tǒng)的SwipeRefreshLayout
的設(shè)計抱有遲疑態(tài)度。
2. 不要私自在dispatchTouchEvent的ACTION_CANCEL時機或者ACTION_UP時機調(diào)用stopNestedScroll方法
??在解釋具體原因联四,我們來看一下NestedScrollingChildHelper
的startNestedScroll
方法和stopNestedScroll
方法。
??stopNestedScroll
方法比較簡單撑教,我們先來看看
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
??stopNestedScroll
表示的意思朝墩,當(dāng)前type的嵌套滑動結(jié)束了,這里主要做的是將對應(yīng)的ViewParent
跟重置為null伟姐。這里為什么需要強調(diào)type呢收苏?通常來說,在正常的滑動中愤兵,stopNestedScroll
只會被調(diào)用一次鹿霸,但是別忘了還有fling滑動,所以type分為兩種:
- TYPE_TOUCH,表示正掣讶椋滑動懦鼠,然后手指松開。
- TYPE_NON_TOUCH,表示手指松開之后還在滑動屹堰。
??所以在RecyclerView
中,一次帶fling操作的滑動stopNestedScroll
方法會被調(diào)用兩次肛冶,一次是ACTION_UP
和ACTION_CANCEL
調(diào)用一次,此時type為TYPE_TOUCH
,一次是fling完畢扯键,此時type為TYPE_NON_TOUCH
睦袖。
??那么將ViewParent
跟重置為null有什么意義呢?這個就得從startNestedScroll
方法得到答案荣刑。
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;
}
??startNestedScroll
先從緩存判斷是否有View可以處理馅笙,然而就是因為這個緩存會導(dǎo)致一個問題。以上面的場景厉亏,SwipeRefreshLayout
私自在ACTION_UP
和ACTION_CANCEL
調(diào)用了stopNestedScroll
方法董习,切斷了它與父View的關(guān)系鏈,但是沒有切斷它與RecyclerView
的關(guān)系鏈叶堆,導(dǎo)致后面再有事件來的話阱飘,只能傳遞到SwipeRefreshLayout
中去,而再也不能傳遞到SwipeRefreshLayout
的父View上去虱颗。
??有人說沥匈,這沒事啊,RecyclerView
也會在ACTION_UP
和ACTION_CANCEL
切斷關(guān)系啊忘渔。但是有沒有考慮到一種情況--就是ACTION_UP
和ACTION_CANCEL
事件不能傳遞到RecylcerView
當(dāng)中高帖。有很多場景都存在這種情況,比如說我們長按RecyclerView的ItemView然后彈出一個Dialog或者浮層畦粮,然后松開散址,這些都有可能導(dǎo)致事件不能傳遞到RecyclerView
中去乖阵。
??我們一旦在ACTION_UP
和ACTION_CANCEL
時切斷SwipeRefreshLayout
與父View的關(guān)系,但是沒有切斷RecyclerView
與SwipeRefreshLayout
的關(guān)系预麸,整個關(guān)系鏈就變成這樣了:
??事件傳遞就變成了這樣:
??從而會導(dǎo)致一種bug瞪浸,在Dialog或者浮層View消失之后第一次滑動中,ViewGroup不能收到事件吏祸,第二次滑動能正常收到对蒲。這是為什么呢?因為第一次滑動之后,
RecyclerView
會調(diào)用stopNestedScroll
方法贡翘;而第二次滑動會重新建立關(guān)系蹈矮,本次關(guān)系鏈就是正常的。
??所以鸣驱,我們千萬不要在ACTION_CANCEL
或者ACTION_UP
時調(diào)用stopNestedScroll
方法泛鸟。研究過RecyclerView
源碼的同學(xué)應(yīng)該都知道,RecyclerView
卻調(diào)用了踊东,這是為什么呢北滥?
這是因為,在整個嵌套滑動關(guān)系鏈中递胧,
RecyclerView
只可能是最底層的View碑韵,也就是只能產(chǎn)生嵌套滑動,不可能作為關(guān)系中間的一員缎脾。這一點祝闻,我們可以從RecyclerView
繼承的接口加以證明,RecyclerView
只實現(xiàn)了NestedScrollingChild
接口遗菠,而沒有實現(xiàn)NestedScrollingParent
接口联喘。
??所以,我們得出一個結(jié)論辙纬,如下:
一旦一個View實現(xiàn)了
NestedScrollingParent
接口豁遭,不能在ACTION_CANCEL
或者ACTION_UP
時調(diào)用stopNestedScroll
方法。說到底就是贺拣,誰是startNestedScroll
的源頭蓖谢,誰才有資格調(diào)用stopNestedScroll
。
??同時,有人可能會問譬涡,如果我們的工程已經(jīng)這么干了闪幽,并且不能修改,或者修改的成本比較大怎么辦呢涡匀?也是有解決方法的盯腌,在這個關(guān)系鏈中,凡是實現(xiàn)了NestedScrollingParent
接口的View
必須在ACTION_CANCEL
或者ACTION_UP
時調(diào)用stopNestedScroll
方法陨瘩。這種方法會強制RecyclerView
在調(diào)用startNestedScroll
方法時腕够,不走緩存级乍,而是重新建立關(guān)系鏈。有一個小小的弊端帚湘,就是fling開始的時候調(diào)用startNestedScroll
方法時本可以使用緩存的玫荣,但是使用此方法之后,會重新建立關(guān)系鏈大诸,性能有所損耗(當(dāng)然這個性能微乎其微崇决,幾乎可以不計??)。
??但是這種方法還有一個比較嚴(yán)重的缺點底挫,就是從此以后fling事件,不能傳遞到ViewGroup
脸侥。這是為什么呢?我們從源碼找一下答案:
??首先建邓,RecyclerView
是在fling之后切斷Type為TYPE_TOUCH
的鏈:
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
}
break;
//----------------------------------------------------------------------------
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll(TYPE_TOUCH);
releaseGlows();
}
??其次通過在fling方法里面,我們都是通過TYPE_TOUCH
的傳遞鏈傳遞事件的:
public boolean fling(int velocityX, int velocityY) {
// ·······
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
// ······
}
return false;
}
??因為我們在dispathcTouchEvent
方法里面就把傳遞鏈給中斷了睁枕,這個中斷肯定在fling之前執(zhí)行官边,進(jìn)而導(dǎo)致fling事件只能傳遞到SwipeRefreshLayou
,而不能傳遞到ViewGroup
(Ps:我們假設(shè)·SwipeRefreshLayou
在dispathcTouchEvent
方法里面就把傳遞鏈給中斷)。這就是fling事件傳遞不過來的根的原因外遇。所以注簿,為了避免各種錯誤,我們千萬不要在私自的調(diào)用stopNestedScroll
方法跳仿。
3. 慎重重寫onStartNestedScroll方法
??我們都知道onStartNestedScroll
方法是用來標(biāo)識當(dāng)前ViewGroup
是消費嵌套滑動的事件诡渴,但是你們不知道這里面也有坑。這里我以一個例子來解釋其中奧妙菲语,同時還會介紹RecyclerView
的一個巨坑妄辩。
??我相信大家都做過RecyclerView
加載更多的功能,如圖:
??大家可能直接看這張圖有點懵逼山上,我來解釋一下:很多時候眼耀,我們使用
RecyclerView
來實現(xiàn)加載更多的功能,當(dāng)加載完成之后佩憾,就讓RecyclerView
停在那里不再動哮伟,可是一旦我們給RecyclerView
套上了一個ViewGroup
之后,用來處理嵌套滑動妄帘,就會出現(xiàn)這種情況:??我來解釋一下上圖中的情況:我們還在加載完成之后楞黄,
RecyclerView
還在繼續(xù)fling。這種情況是不能容忍的寄摆,怎么來解決呢谅辣?這就需要正確的重寫onStartNestedScroll
方法,最簡單和正確的方法是我們在重寫onStartNestedScroll
方法時婶恼,必須對type進(jìn)行判斷桑阶,代碼如下:
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes, @ViewCompat.NestedScrollType int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && type == ViewCompat.TYPE_TOUCH;
}
??我們在onStartNestedScroll
方法對type進(jìn)行了判斷柏副,這也是我們重寫onStartNestedScroll
方法時非常容易忽視的點。
??問題倒是解決了蚣录,可是大家肯定好奇為什么會出現(xiàn)這種情況割择,同時為什么加了type的判斷就能解決呢?
??首先萎河,我先來解釋一下為什么會這種情況荔泳,其實答案是非常的簡單,在加載完成過程中虐杯,ViewFlinger
還在繼續(xù)fling玛歌,當(dāng)數(shù)據(jù)回來時,此時fling事件還未完成擎椰,新數(shù)據(jù)加載到RecyclerView
中去支子,ViewFlinger
發(fā)現(xiàn)此時已經(jīng)有空間可以滑動了,那么就會繼續(xù)滑動达舒。我自己覺得這是RecyclerView
挖的一個坑值朋。
??其次,我們來看一下巩搏,為什么加上type判斷就能解決問題呢昨登?我們從RecyclerView
的fling方法尋找答案:
public boolean fling(int velocityX, int velocityY) {
// ······
// 1. 分發(fā)fling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
// ······
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 2. 建立type為TYPE_NON_TOUCH的傳遞鏈
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
??在fling
方法里面,做了比較重要兩件事:
- 分發(fā)
fling
事件贯底。如果我們在處理嵌套滑動丰辣,很少會自己處理fling
事件,所以dispatchNestedPreFling
方法通常返回為false禽捆,從而進(jìn)入了if的判斷語句中糯俗。- 通過
startNestedScroll
方法建立type為TYPE_NON_TOUCH
的嵌套滑動傳遞鏈。由于睦擂,我們在上層View中沒有對type進(jìn)行判斷得湘,所以最終的傳遞鏈中會有我們的ViewGroup
。
??然后顿仇,我們再來看看ViewFlinger
run方法的一段代碼:
if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
TYPE_NON_TOUCH)
&& (overscrollX != 0 || overscrollY != 0)) {
final int vel = (int) scroller.getCurrVelocity();
int velX = 0;
if (overscrollX != x) {
velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
}
int velY = 0;
if (overscrollY != y) {
velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
}
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
absorbGlows(velX, velY);
}
if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
&& (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
scroller.abortAnimation();
}
}
??這段代碼中的作用就是淘正,當(dāng)fling的速度為0時或者滑動的距離為0時,會通過abortAnimation
來中斷后面的fling臼闻。因為我們在startNestedScroll
成功的建立傳遞鏈鸿吆,所以在這里dispatchNestedScroll
肯定為true,所以永遠(yuǎn)走不到這段邏輯述呐,最終就會導(dǎo)致上面出現(xiàn)的那個問題惩淳。
??而我們在我們ViewGroup的onStartNestedScroll
方法對type加上了判斷,在建立的傳遞鏈中不會有我們得ViewGroup
,所以dispatchNestedScroll
方法就會返回為false,在滑不動時思犁,自然就會中斷未完成的fling代虾。最終我們證實了上面的解決方法為什么是正確的,而不是通過一種hack方式來實現(xiàn)激蹲。
到此棉磨,我就對此坑的分析就結(jié)束了。綜上所述学辱,我們在重寫
onStartNestedScroll
方法一定要小心乘瓤,一定要考慮到type為TYPE_NON_TOUCH
的情況。
4. 總結(jié)
??最后策泣,我在此說幾句衙傀,嵌套滑動是爸爸給我們的好東西,但是我也們不能亂用萨咕,否則出了問題真的是太難找的根本原因了差油,血的教訓(xùn)啊H味础!发侵!????