前言
先上圖。
請注意看開頭部分
這里引用的是一張我之前寫的一個(gè)組件的動圖,很明顯大家可以在我開頭看到一個(gè)很流暢的下拉上拉的操作,沒有任何阻礙,這就是我所說的完美解決嵌套滑動沖突方案。
接下來的話請注意:
閱讀本文需要對Android的事件分發(fā)機(jī)制有一定的了解,如果不了解我建議先去了解一下Android的事件分發(fā)機(jī)制!
言歸正傳:
這里我打算通過一步步的模擬我們要遇到問題的情景,再一步步的解決這些問題,最終呈現(xiàn)出一個(gè)完美的解決方案。
1.解決嵌套滑動沖突第一步,攔截事件
情景1: 首先我們在自己寫的容器中(可以滑動的容器都可以)添加一個(gè) RecyclerView(只要是可以滑動的組件就可以,此處我以 RecyclerView為例),當(dāng)遇到這種場景,我們就會發(fā)現(xiàn)無論我們怎么滑動 RecyclerView,父容器都不會滑動。
這時(shí)候了解過事件分發(fā)機(jī)制的朋友就很明顯的可以發(fā)現(xiàn),事件一直被RecyclerView消費(fèi)了,父容器并沒有消費(fèi)到事件。
所以這里我已下拉為例,來提供第一個(gè)解決方案:
列表在頂部,且下拉時(shí),容器將事件攔截:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mInitialDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int)ev.getY();
int offsetY = mInitialDownY - nowY;
mInitialDownY = nowY;
if(!rvView.canScrollVertically(-1)) {
if (offsetY < 0) {//判斷子view是否滑動到頂部并且當(dāng)前是下拉
return true;//是的就攔截事件
}
}
break;
}
return false;
}
如果你這樣寫了并運(yùn)行后,很容易的就發(fā)現(xiàn)會出現(xiàn)一個(gè)問題:
情景2: 列表自身已經(jīng)在滑動,滑動到最頂部時(shí),繼續(xù)下拉,事件沒有被容器攔截,需要重新松手,按下再下拉,容器才會攔截事件。
很明顯是因?yàn)榱斜磉€在持有事件。
那這時(shí)候的思路也很明確,在列表滾動時(shí),到達(dá)最頂部或者底部,將事件還給父容器。
2.讓子 View “交還”事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDispatchDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int)ev.getY();
int offsetY = mDispatchDownY-nowY;
mDispatchDownY = nowY;
if((!rvView.canScrollVertically(-1)&&offsetY<0)||(!rvView.canScrollVertically(1)&&offsetY>0)){
//子 View 到達(dá)頂部或者底部,且滑動方向符合邏輯時(shí),將事件還給父容器
//此處應(yīng)該再添加相關(guān)邏輯避免父容器消費(fèi)事件時(shí)頻繁調(diào)用此方法
requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
上面代碼中出現(xiàn)了這個(gè)方法
requestDisallowInterceptTouchEvent(false);
這個(gè)方法用途是告訴容器可以攔截事件,如果參數(shù)是 true 的話就是不要攔截事件。
順便來看下源碼好了:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
//此處判斷當(dāng)前mGroupFlags是不是FLAG_DISALLOW_INTERCEPT,如果是則說明當(dāng)前這個(gè)ViewGroup已經(jīng)是FLAG_DISALLOW_INTERCEPT狀態(tài)了,后面的代碼沒必要再執(zhí)行了
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;//如果disallowIntercept為true,則將ViewGroup狀態(tài)置為FLAG_DISALLOW_INTERCEPT
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//反之就取消掉這個(gè)標(biāo)記
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);//從這里看出來 這個(gè)方法是遞歸的
}
}
其實(shí)requestDisallowInterceptTouchEvent
方法的源碼很簡單,看注釋就知道它是怎么實(shí)現(xiàn)的了,有意思的是這個(gè)方法是遞歸的,所以一旦調(diào)用了requestDisallowInterceptTouchEvent(true)
后,就會將當(dāng)前ViewGroup以及所有包含了它的ViewGroup都置為FLAG_DISALLOW_INTERCEPT
這個(gè)標(biāo)記,這個(gè)標(biāo)記看名字就知道是立了一個(gè)不攔截事件的flag。
而我們通過調(diào)用requestDisallowInterceptTouchEvent(false)
來告訴容器可以攔截事件,達(dá)到一個(gè)事件"交還"給父容器的效果。
至于為什么調(diào)用這個(gè)方法就能解決這個(gè)問題,其實(shí)和下一步的知識點(diǎn)有很大的關(guān)聯(lián),在后面一起做一個(gè)分析。
3.父容器持有事件時(shí)將事件轉(zhuǎn)交給子View
這一步就是完美解決嵌套滑動沖突的最后一步,讓我們來模擬一下發(fā)生這種情況的場景:
情景3: 當(dāng)我們父容器再滑動時(shí),滑動到某一個(gè)位置,或者說這個(gè)時(shí)候 按照我們寫好的判斷是否該攔截這個(gè)事件的方法 來判斷出我們應(yīng)該將事件交給子 View,讓子 View 可以滑動,我們就發(fā)現(xiàn)了,此時(shí)子 View 并不會滑動,還是父容器在滑動。
這里依舊可以很明顯的看出,父容器并沒有把事件發(fā)給子 View。
所以我們追尋一下之前寫的代碼,發(fā)現(xiàn)在父容器里目前只在onInterceptTouchEvent
方法中處理了相關(guān)邏輯,那秉著遇事不解看源碼的原則,我們?nèi)タ聪?code>onInterceptTouchEvent是在什么時(shí)候被執(zhí)行的,不難發(fā)現(xiàn),onInterceptTouchEvent
是在ViewGroup的dispatchTouchEvent
方法中被執(zhí)行的,這里我貼出相關(guān)的源碼:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();//4
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//2
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//3
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//5
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
先來看注釋1, 這里是個(gè)判斷,判斷條件就是當(dāng)前事件是ACTION_DOWN
且mFirstTouchTarget
不為null的情況,執(zhí)行下一步,這里ACTION_DOWN
就是第一個(gè)手指接觸屏幕的時(shí)候產(chǎn)生的事件,mFirstTouchTarget
則是可以接受事件的View。
再來看注釋2, 這個(gè)地方是不是很熟悉,我們又看到了FLAG_DISALLOW_INTERCEPT
,沒錯(cuò),這就是我們調(diào)用requestDisallowInterceptTouchEvent
方法時(shí)相關(guān)的一個(gè)FLAG(其實(shí)這個(gè)狀態(tài)只有調(diào)用requestDisallowInterceptTouchEvent(true)
時(shí)會被設(shè)置),所以這里的邏輯就是 當(dāng)前狀態(tài)不為FLAG_DISALLOW_INTERCEPT
時(shí),disallowIntercept
為 false,反之為 true。
插入注釋4: 此處會關(guān)閉FLAG_DISALLOW_INTERCEPT
狀態(tài),所以每次ACTION_DOWN
時(shí)都會重置這個(gè)狀態(tài)。
最后看注釋3, 這里會執(zhí)行我們需要的onInterceptTouchEvent
方法,所以到現(xiàn)在我們得出結(jié)論,onInterceptTouchEvent
方法,只有當(dāng)前狀態(tài)為ACTION_DOWN
且mFirstTouchTarget
不為null的情況,當(dāng)前ViewGroup狀態(tài)不為FLAG_DISALLOW_INTERCEPT
時(shí),才會被調(diào)用,而且在情景3發(fā)生時(shí),我們已經(jīng)攔截過事件(不然事件不會由父容器消費(fèi)),說明當(dāng)前沒有可以接收事件的子View。
為什么調(diào)用requestDisallowInterceptTouchEvent(false)
可以“交還”事件,看到這里我們先把上面拋出的這個(gè)問題解決了,當(dāng)子 View 在滑動時(shí),像RecyclerView這些可滑動的組件,消費(fèi)事件時(shí)內(nèi)部一般都會調(diào)用getParent().requestDisallowInterceptTouchEvent(true)
方法,將其所有的父容器的狀態(tài)都標(biāo)記為FLAG_DISALLOW_INTERCEPT
,實(shí)現(xiàn)一個(gè)長期持有事件,只有觸發(fā)ACTION_DOWN
或者調(diào)用getParent().requestDisallowInterceptTouchEvent(false)
時(shí)會重置這個(gè)狀態(tài),此時(shí)父容器mFirstTouchTarget
不為空,所以不需要ACTION_DOWN
也可以有機(jī)會執(zhí)行onInterceptTouchEvent
方法,容器調(diào)用requestDisallowInterceptTouchEvent(false)
,關(guān)閉狀態(tài)FLAG_DISALLOW_INTERCEPT
,此時(shí)注釋2中disallowIntercept
為 false,此時(shí)可以執(zhí)行onInterceptTouchEvent
方法,父容器經(jīng)過判斷,攔截事件。
解決了上面的問題我們再來得出一個(gè)結(jié)論: 父容器滑動時(shí),不會執(zhí)行 onInterceptTouchEvent
方法把事件分發(fā)給子View。
解決方案也很直接: 模擬一次ACTION_DOWN
事件,觸發(fā)onInterceptTouchEvent
方法,分發(fā)事件給子 View。
先上代碼
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDispatchDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
mLastMoveMotionEvent = ev;//緩存最后的事件
//計(jì)算偏移量
int nowY = (int)ev.getY();
int offsetY = mDispatchDownY-nowY;
mDispatchDownY = nowY;
if(...){//判斷條件,在合適的時(shí)候模擬`ACTION_DOWN`事件
sendDownEvent(mLastMoveMotionEvent);
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* @param ev
* 模擬down事件
*/
private void sendDownEvent(MotionEvent ev){
MotionEvent e = MotionEvent.obtain(ev.getDownTime(),ev.getEventTime(), MotionEvent.ACTION_DOWN,ev.getX(),ev.getY(),ev.getMetaState());
super.dispatchTouchEvent(e);
}
調(diào)用MotionEvent.obtain
,模擬一個(gè) down 事件,容器重新調(diào)用攔截方法,分發(fā)事件給子 View ,此時(shí)無阻礙的嵌套滑動實(shí)現(xiàn)。
總結(jié)
我說的完美解決嵌套滑動沖突就是上面這三步,但是實(shí)現(xiàn)這三步需要我們對Android事件分發(fā)機(jī)制要有一個(gè)清晰的了解,我們不應(yīng)該局限于基本的事件分發(fā)教程中告訴你的這里返回 true 我們就攔截了事件,那里返回 false 我們就...
秉著遇事不解看源碼的原則
而要更深的去了解Android事件分發(fā),去解決遇到的相關(guān)問題,從上文的源碼分析中,我們可以通過一小段代碼分析出這么多的問題所在,所以解決問題還是要回歸源碼,從源碼分析,找出問題,解決問題。
最后我放一張解決方案的流程圖做一個(gè)總結(jié):
- 首先需要在
onInterceptTouchEvent
方法中判斷是否攔截 - 當(dāng)子 View消費(fèi)事件時(shí),判斷不需要事件的時(shí)候調(diào)用
requestDisallowInterceptTouchEvent(false)
讓父容器可以重新攔截事件 - 當(dāng)父容器消費(fèi)事件時(shí),判斷不需要事件的時(shí)候模擬
ACTION_DOWN
事件重新執(zhí)行onInterceptTouchEvent
方法,將事件發(fā)給子 View