Android 源碼分析問題(三)—— 通過事件分發(fā)完美解決嵌套滑動沖突

前言

先上圖。

請注意看開頭部分

DampRefreshAndLoadMoreLayout

這里引用的是一張我之前寫的一個(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_DOWNmFirstTouchTarget不為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_DOWNmFirstTouchTarget不為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é):

完美解決嵌套滑動沖突.png
  1. 首先需要在onInterceptTouchEvent方法中判斷是否攔截
  2. 當(dāng)子 View消費(fèi)事件時(shí),判斷不需要事件的時(shí)候調(diào)用requestDisallowInterceptTouchEvent(false)讓父容器可以重新攔截事件
  3. 當(dāng)父容器消費(fèi)事件時(shí),判斷不需要事件的時(shí)候模擬ACTION_DOWN事件重新執(zhí)行onInterceptTouchEvent方法,將事件發(fā)給子 View
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,667評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容