教你用兩招就完美解決 Android 滑動衝突!

教你用兩招就完美解決 Android 滑動衝突!

衝突狀況

在 Android 開發中,滑動衝突老是咱們一個沒法避免的話題。而對於解決方案倒是衆說紛紜。好比 RecyclerView 嵌套 RecyclerView,直接經過相關方法禁掉內部 RecyclerView 的滑動;ScrollView 嵌套 RecyclerView 直接把 ScrollView 替換爲 NestedScrollView 等等。程序員

但咱們今天要說的是在自定義 View 中遇到滑動衝突時,咱們又應該如何處理呢?固然,今天的話題須要 View 的事件分發機制作理論前提。面試

1. 簡單介紹 View 的事件分發機制

固然,這裏也能夠簡單地提一下,基本的流程就是下面的僞代碼。架構

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

當一個 ViewGroup 接收到一個事件的時候,首先會調用 dispatchTouchEvent() 方法進行事件分發,若是 onInterceptTouchEvent() 返回 true,則表明當前 View 會攔截事件,則直接回調 onTouchEvent() 方法進行事件處理。ide

若是不攔截,則直接回調子 View 的 dispatchTouchEvent() 方法。如此反覆,一直到最裏面的子 View。學習

當一個點擊事件產生後,它的傳遞過程遵循如下順序:
Activity => Window => View,即事件老是先傳遞給 Activity,Activity 再傳遞給 Window,最後 Window 再傳遞給頂層 DecorView,而後遵循上面的方式一直在最裏層 View。spa

而處理事件則從最裏層 View 不斷回傳給本身的外層 View,若是一直沒有 View 進行處理,則直接會回傳到 Activity 中。3d

onTouchEvent() 返回 true 表明本身要處理。code

既然都提了這麼一點,也就忽然想給出一些結論,參考自 Android 開發藝術探索:視頻

1.同一個事件序列是指從手指接觸屏幕(ACTION_DOWN)的那一刻起,到手指離開屏幕(ACTION_UP)的那一刻結束,中間含不定數量的 ACTION_MOVE 事件。
2.某個 View 一旦決定攔截事件,那麼這一個事件序列都只能由它處理,而且它的 onInterceptTouchEvent() 方法也不會再調用。換句話說,好比一個 ViewGroup 裏面有數個子 View,一旦 ACTION_DOWN 事件從 Activity 傳到這個 ViewGroup 被其攔截,則後續的 MOVE 和 UP 等事件也不會傳遞到裏面的子 View 中。
3.若是一個 View 一旦開始處理事件,若是它不消耗 ACTION_DOWN 事件,即 onTouchEvent()返回爲 false,那麼同一事件序列中的其餘事件也不會再交給它處理,直接會調用其父 View 的 onTouchEvent()。
4.若是 View 不消耗除 ACTION_DOWN 之外的其餘事件,那麼這個點擊事件會消失,此時父元素的 onTouchEvent() 並不會被調用,而且固然 View 能夠持續收到後續的事件,最終這些消失的點擊事件會傳遞給 Activity 處理。
5.ViewGroup 默認不攔截事件,View 沒有 onInterceptTouchEvent() 方法,一旦有事件傳遞給它,則直接會調用 onTouchEvent(),而且起默認都會消耗掉事件。除非它是不可點擊的(即 clickable 和 longClickable 均爲 false)。View 的 longClickable 默認都爲 false,而 clickable 分狀況,好比 Button 默認爲 true,TextView 默認爲 false。
6.Viewenable 屬性不會影響 onTouchEvent() 的默認返回值,哪怕一個 View 是 disable 狀態的,只要它的 clickable 或者 longClickable 有一個爲 true,那麼它的 onTouchEvent() 就會返回 true。
7.requestDisallowInterceptTouchEvent() 能夠在子元素中干預父元素的事件分發過程,可是沒法干預 ACTION_DOWN 事件。
8.事件優先順序:
setOnTouchListener() => onTouchEvent() => onClickListener()blog

一不當心發現仍是挺多的,固然這些都是結論。

2. 處理自定義 View 中的滑動衝突

對於大多數 Android 開發來講,處理滑動衝突好像很難,但實戰一下又發現,好像也挺簡單,由於這個其實是有套路可循的。基本就兩種方案:外部攔截法 && 內部攔截法。

2.1 外部攔截法

所謂外部攔截法,顧名思義,就是直接在父容器中直接攔截掉咱們的滑動事件,讓其不能進入到子元素中,這彷佛和咱們 RecyclerView 嵌套 RecyclerView 時禁用內部 RecyclerView 滑動有那麼一絲類似之處,就是內部不處理就完事兒了。但細細品來又徹底不同,這裏的外部攔截法會讓內部元素根本就收不到滑動事件。

這種方法明顯很是適合咱們上面講的事件分發機制。咱們在接收 ACTION_MOVE 事件的時候,直接經過使 onInterceptTouchEvent() 方法返回 true 來直接攔截掉事件就能夠了,僞代碼想必你們也知道了:

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

代碼很簡單,咱們僅僅須要在事件 ACTION_MOVE 時去處理咱們的邏輯就行了,當知足咱們的邏輯的時候,就攔截掉 ACTION_MOVE 事件給本身處理。

至於爲何不去攔截 ACTION_DOWN 和 ACTION_UP,想必你們也清楚了。上面說了,若是攔截了 ACTION_DOWN 事件,那後續的 ACTION_MOVE、ACTION_UP 等其它事件均不會在調用 onInterceptTouchEvent() 方法,會直接交給當前容器處理。而若是咱們攔截掉 ACTION_UP 的話,確定會致使子元素的點擊事件沒法被處理,由於你們確定都知道一個點擊事件從 ACTION_DOWN 開始,從 ACTION_UP 結束,兩者缺一不可。

2.2 內部攔截法

內部攔截法相對外部攔截法會複雜一些,因此咱們一般來講,都更加推薦用外部攔截法進行處理。不過,內部攔截法依然有着它很是重要的地位,具體狀況有可能會遇到。

內部攔截法的話,須要 requestDisallowInterceptTouchEvent() 方法的支持,這個方法是幹什麼的呢?顧名思義,請求是否不容許攔截事件,其接收一個 boolean 參數,表示是否不容許攔截。

咱們直接重寫子元素的 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)
}

想必代碼也是很是簡單易懂的,咱們給父容器的 requestDisallowInterceptTouchEvent() 傳遞的參數表明是否不容許其攔截事件,當參數爲 true 的時候表明不容許攔截,爲 false 的時候表明攔截。因此看起來和外部攔截法也就一模一樣了。

不過僅僅有這點修改還不夠,咱們經過前面的理論基礎知道,當咱們的父容器攔截掉 ACTION_DOWN 事件的時候,全部的事件都沒法再傳遞到子元素中,天然也就不會調用上面咱們寫的 dispatchTouchEvent() 方法了。因此咱們在內部攔截法的時候還須要重寫父容器的 onInterceptTouchEvent() 方法。

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

至此,基本介紹了兩種處理滑動衝突的解決方案,在自定義 View 的時候結合實際場景也就能夠駕輕就熟了。

文章提到的問題你遇到過嗎?是怎麼解決的?


最後對於程序員來講,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提高本身,歷來都是咱們去適應環境,而不是環境來適應咱們!

這裏附上上述的技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了很多精力),包含知識脈絡 + 諸多細節,因爲篇幅有限,這裏以圖片的形式給你們展現一部分。

相信它會給你們帶來不少收穫:

教你用兩招就完美解決 Android 滑動衝突!

上述【高清技術腦圖】以及【配套的架構技術PDF】能夠 加我wx:X1524478394 免費獲取

當程序員容易,當一個優秀的程序員是須要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每一個階段都須要掌握不一樣的能力。早早肯定本身的職業方向,才能在工做和能力提高中甩開同齡人。

相關文章
相關標籤/搜索