每日一問:Android 如何處理滑動(dòng)沖突

堅(jiān)持原創(chuàng)日更,短平快的 Android 進(jìn)階系列,敬請(qǐng)直接在微信公眾號(hào)搜索:nanchen,直接關(guān)注并設(shè)為星標(biāo),精彩不容錯(cuò)過。

在 Android 開發(fā)中,滑動(dòng)沖突總是我們一個(gè)無法避免的話題。而對(duì)于解決方案卻是眾說紛紜。比如 RecyclerView 嵌套 RecyclerView,直接通過相關(guān)方法禁掉內(nèi)部 RecyclerView 的滑動(dòng);ScrollView 嵌套 RecyclerView 直接把 ScrollView 替換為 NestedScrollView 等等。但我們今天要說的是在自定義 View 中遇到滑動(dòng)沖突時(shí),我們又應(yīng)該如何處理呢?

當(dāng)然,今天的話題需要 View 的事件分發(fā)機(jī)制做理論前提,還不了解 View 的事件分發(fā)機(jī)制的小伙伴可以移步我之前面試系列的一篇文章:面試系列:講講 Android 的事件分發(fā)機(jī)制。

簡(jiǎn)單介紹 View 的事件分發(fā)機(jī)制

當(dāng)然,這里也可以簡(jiǎn)單地提一下,基本的流程就是下面的偽代碼。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

當(dāng)一個(gè) ViewGroup 接收到一個(gè)事件的時(shí)候,首先會(huì)調(diào)用 dispatchTouchEvent() 方法進(jìn)行事件分發(fā),如果 onInterceptTouchEvent() 返回 true,則代表當(dāng)前 View 會(huì)攔截事件,則直接回調(diào) onTouchEvent() 方法進(jìn)行事件處理。如果不攔截,則直接回調(diào)子 View 的 dispatchTouchEvent() 方法,如此反復(fù),一直到最里面的子 View。

當(dāng)一個(gè)點(diǎn)擊事件產(chǎn)生后,它的傳遞過程遵循以下順序:Activity => Window => View,即事件總是先傳遞給 ActivityActivity 再傳遞給 Window,最后 Window 再傳遞給頂層 DecorView,然后遵循上面的方式一直在最里層 View

而處理事件則從最里層 View 不斷回傳給自己的外層 View,如果一直沒有 View 進(jìn)行處理,則直接會(huì)回傳到 Activity 中。

onTouchEvent() 返回 true 代表自己要處理。

既然都提了這么一點(diǎn),也就突然想給出一些結(jié)論,參考自 Android 開發(fā)藝術(shù)探索:

  1. 同一個(gè)事件序列是指從手指接觸屏幕(ACTION_DOWN)的那一刻起,到手指離開屏幕(ACTION_UP)的那一刻結(jié)束,中間含不定數(shù)量的 ACTION_MOVE 事件。
  2. 某個(gè) View 一旦決定攔截事件,那么這一個(gè)事件序列都只能由它處理,并且它的 onInterceptTouchEvent() 方法也不會(huì)再調(diào)用。換句話說,比如一個(gè) ViewGroup 里面有數(shù)個(gè)子 View,一旦 ACTION_DOWN 事件從 Activity 傳到這個(gè) ViewGroup 被其攔截,則后續(xù)的 MOVE 和 UP 等事件也不會(huì)傳遞到里面的子 View 中。
  3. 如果一個(gè) View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件,即 onTouchEvent() 返回為 false,那么同一事件序列中的其他事件也不會(huì)再交給它處理,直接會(huì)調(diào)用其父 View 的 onTouchEvent()
  4. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的 onTouchEvent() 并不會(huì)被調(diào)用,并且當(dāng)然 View 可以持續(xù)收到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給 Activity 處理。
  5. ViewGroup 默認(rèn)不攔截事件,View 沒有 onInterceptTouchEvent() 方法,一旦有事件傳遞給它,則直接會(huì)調(diào)用 onTouchEvent(),并且起默認(rèn)都會(huì)消耗掉事件。除非它是不可點(diǎn)擊的(即 clickablelongClickable 均為 false)。View 的 longClickable 默認(rèn)都為 false,而 clickable 分情況,比如 Button 默認(rèn)為 true,TextView 默認(rèn)為 false。
  6. View 的 enable 屬性不會(huì)影響 onTouchEvent() 的默認(rèn)返回值,哪怕一個(gè) Viewdisable 狀態(tài)的,只要它的 clickable 或者 longClickable 有一個(gè)為 true,那么它的 onTouchEvent() 就會(huì)返回 true。
  7. requestDisallowInterceptTouchEvent() 可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是無法干預(yù) ACTION_DOWN 事件。
  8. 事件優(yōu)先順序:setOnTouchListener() => onTouchEvent() => onClickListener()

一不小心發(fā)現(xiàn)還是挺多的,當(dāng)然這些都是結(jié)論,具體可以跟著 面試系列:講講 Android 的事件分發(fā)機(jī)制 進(jìn)行源碼流程探討,你會(huì)發(fā)現(xiàn)上面的結(jié)論很容易得到。

處理自定義 View 中的滑動(dòng)沖突

對(duì)于大多數(shù) Android 開發(fā)來說,處理滑動(dòng)沖突好像很難,但實(shí)戰(zhàn)一下又發(fā)現(xiàn),好像也挺簡(jiǎn)單,因?yàn)檫@個(gè)實(shí)際上是有套路可循的?;揪蛢煞N方案:外部攔截法 && 內(nèi)部攔截法。

外部攔截法

所謂外部攔截法,顧名思義,就是直接在父容器中直接攔截掉我們的滑動(dòng)事件,讓其不能進(jìn)入到子元素中,這似乎和我們 RecyclerView 嵌套 RecyclerView 時(shí)禁用內(nèi)部 RecyclerView 滑動(dòng)有那么一絲相似之處,就是內(nèi)部不處理就完事兒了。但細(xì)細(xì)品來又完全不一樣,這里的外部攔截法會(huì)讓內(nèi)部元素根本就收不到滑動(dòng)事件。

這種方法明顯非常適合我們上面講的事件分發(fā)機(jī)制。我們?cè)诮邮?ACTION_MOVE 事件的時(shí)候,直接通過使 onInterceptTouchEvent() 方法返回 true 來直接攔截掉事件就可以了,偽代碼想必大家也知道了:

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    ev?.run { 
        if (action == MotionEvent.ACTION_MOVE && 父容器需要點(diǎn)擊事件){
            return true
        }
    }
    return super.onInterceptTouchEvent(ev)
}

代碼很簡(jiǎn)單,我們僅僅需要在事件 ACTION_MOVE 時(shí)去處理我們的邏輯就好了,當(dāng)滿足我們的邏輯的時(shí)候,就攔截掉 ACTION_MOVE 事件給自己處理。

至于為什么不去攔截 ACTION_DOWNACTION_UP,想必大家也清楚了。上面說了,如果攔截了 ACTION_DOWN 事件,那后續(xù)的 ACTION_MOVE、ACTION_UP 等其它事件均不會(huì)在調(diào)用 onInterceptTouchEvent() 方法,會(huì)直接交給當(dāng)前容器處理。而如果我們攔截掉 ACTION_UP 的話,肯定會(huì)導(dǎo)致子元素的點(diǎn)擊事件無法被處理,因?yàn)榇蠹铱隙ǘ贾酪粋€(gè)點(diǎn)擊事件從 ACTION_DOWN 開始,從 ACTION_UP 結(jié)束,二者缺一不可。

內(nèi)部攔截法

內(nèi)部攔截法相對(duì)外部攔截法會(huì)復(fù)雜一些,所以我們通常來說,都更加推薦用外部攔截法進(jìn)行處理。不過,內(nèi)部攔截法依然有著它非常重要的地位,具體情況有可能會(huì)遇到。

內(nèi)部攔截法的話,需要 requestDisallowInterceptTouchEvent() 方法的支持,這個(gè)方法是干什么的呢?顧名思義,請(qǐng)求是否不允許攔截事件,其接收一個(gè) boolean 參數(shù),表示是否不允許攔截。

我們直接重寫子元素的 dispatchTouchEvent() 方法,得到偽代碼如下:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    ev?.run { 
        when(action){
            MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)
            MotionEvent.ACTION_MOVE ->{
                if(滿足需要讓外部容器攔截事件){
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
    }
    return super.dispatchTouchEvent(ev)
}

想必代碼也是非常簡(jiǎn)單易懂的,我們給父容器的 requestDisallowInterceptTouchEvent() 傳遞的參數(shù)代表是否不允許其攔截事件,當(dāng)參數(shù)為 true 的時(shí)候代表不允許攔截,為 false 的時(shí)候代表攔截。所以看起來和外部攔截法也就如出一轍了。

不過僅僅有這點(diǎn)修改還不夠,我們通過前面的理論基礎(chǔ)知道,當(dāng)我們的父容器攔截掉 ACTION_DOWN 事件的時(shí)候,所有的事件都無法再傳遞到子元素中,自然也就不會(huì)調(diào)用上面我們寫的 dispatchTouchEvent() 方法了。所以我們?cè)趦?nèi)部攔截法的時(shí)候還需要重寫父容器的 onInterceptTouchEvent() 方法。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    ev?.run { 
        if (action == MotionEvent.ACTION_DOWN){
            return false
        }
    }
    return super.onInterceptTouchEvent(ev)
}

至此,基本介紹了兩種處理滑動(dòng)沖突的解決方案,在自定義 View 的時(shí)候結(jié)合實(shí)際場(chǎng)景也就可以得心應(yīng)手了。

除了滑動(dòng)沖突,滑動(dòng)處理也是一項(xiàng)非常有意思的工作,感興趣的可以可以參考 NestedScrollingParent2NestedScrollingChild2 喲。

文章參考自:《Android 開發(fā)藝術(shù)探索》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容