Android事件分發機制二:viewGroup與view對事件的處理

前言

很高興碰見你~css

在上一篇文章 Android事件分發機制一:事件是如何到達activity的? 中,咱們討論了觸摸信息從屏幕產生到發送給具體 的view處理的總體流程,這裏先來簡單回顧一下:java

總體流程

  1. 觸摸信息從手機觸摸屏幕時產生,經過IMS和WMS發送到viewRootImpl
  2. viewRootImpl把觸摸信息傳遞給他所管理的view
  3. view根據自身的邏輯對事件進行分發
  4. 常見的如Activity佈局的頂層viewGroup爲DecorView,他對事件分發方法進行了從新,會優先回調windowCallBack也就是Activity的分發方法
  5. 最後事件都會交給viewGroup去分發給子view

前面的分發步驟咱們清楚了,那麼viewGroup是如何對觸摸事件進行分發的呢?View又是如何處理觸摸信息的呢?正是本文要討論的內容。android

事件處理中涉及到的關鍵方法就是 dispatchTouchEvent ,無論是viewGroup仍是view。在viewGroup中,dispatchTouchEvent 方法主要是把事件分發給子view,而在view中,dispatchTouchEvent 主要是處理消費事件。而主要的消費事件內容是在 onTouchEvent 方法中。下面討論的是viewGroup與view的默認實現,而在自定義view中,一般會重寫 dispatchTouchEventonTouchEvent 方法,例如DecorView等。git

秉着邏輯先行源碼後到的原則,本文雖然涉及到大量的源碼,但會優先講清楚流程,有時間的讀者仍然建議閱讀完整源碼。面試

理解MotionEvent

事件分發中涉及到一個很重要的點:多點觸控,這是在不少的文章中沒有體現出來的。而要理解viewGroup如何處理多點觸控,首先須要對觸摸事件信息類:MotionEvent,有必定的認識。MotionEvent中承載了觸摸事件的不少信息,理解它更有利於咱們理解viewGroup的分發邏輯。因此,首先須要先理解MotionEvent。api

觸摸事件的基本類型有三種:數組

  • ACTION_DOWN: 表示手指按下屏幕
  • ACTION_MOVE: 手指在屏幕上滑動時,會產生一系列的MOVE事件
  • ACTION_UP: 手指擡起,離開屏幕

一個完整的觸摸事件系列是:從ACTION_DOWN開始,到ACTION_UP結束 。這其實很好理解,就是手指按下開始,手指擡起結束。安全

手指可能會在屏幕上滑動,那麼中間會有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP。併發

這是正常的狀況,而若是出現了一些異常的狀況,事件序列被中斷,那麼會產生一個取消事件:ide

  • ACTION_CANCEL:當出現異常狀況事件序列被中斷,會產生該類型事件

因此,完整的事件序列是:從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束 。固然,這是咱們一個手指的狀況,那麼在多指操做的狀況是怎麼樣的呢?這裏須要引入另外的事件類型:

  • ACTION_POINTER_DOWN: 當已經有一個手指按下的狀況下,另外一個手指按下會產生該事件
  • ACTION_POINTER_UP: 多個手指同時按下的狀況下,擡起其中一個手指會產生該事件

區別於ACTION_DOWN和ACTION_UP,使用另外兩個事件類型來表示手指的按下與擡起,使得ACTION_DOWN和ACTION_UP能夠做爲一個完整的事件序列的邊界

同時,一個手指的事件序列,是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束。

到這裏先簡單作個小結:

觸摸事件的類型有:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_DOWN、ACTION_POINTER_UP,他們分別表明不一樣的場景。

一個完整的事件序列是從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束。
一個手指的完整序列是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束。


第二,咱們須要理解MotionEvent中所攜帶的信息。

假如如今屏幕上有兩個手指按下,以下圖:

觸摸點a先按下,而觸摸點b按下,那麼天然而然就會產生兩個事件:ACTION_DOWN和ACTION_POINTER_DOWN。那麼是否是ACTION_DOWN事件就只包含有觸摸點a的信息,而ACTION_POINTER_DOWN只包含觸摸點b的信息呢?換句話說,這兩個事件是否是會獨立發出觸摸事件?答案是:不是。

每個觸摸事件中,都包含有全部觸控點的信息。例如上述的點b按下時產生的ACTION_POINTER_DOWN事件中,就包含了觸摸點a和觸摸點b的信息。那麼他是如何區分這兩個點的信息?咱們又是如何知道ACTION_POINTER_DOWN這個事件類型是屬於觸摸點a仍是觸摸點b?

在MotionEvent對象內部,維護有一個數組。這個數組中的每一項對應不一樣的觸摸點的信息,以下圖:

image.png

數組下標稱爲觸控點的索引,每一個節點,擁有一個觸控點的完整信息。這裏要注意的是,一個觸控點的索引並非一成不變的,而是會隨着觸控點的數目變化而變化。例如當同時按下兩個手指時,數組狀況以下圖:

image.png

而當手指a擡起後,數組的狀況變爲下圖:

image.png

能夠看到觸控點b的索引改變了。因此跟蹤一個觸控點必須是依靠一個觸控點的id,而不是他的索引

如今咱們知道每個MotionEvent內部都維護有全部觸控點的信息,那麼咱們怎麼知道這個事件是對應哪一個觸控點呢?這就須要看到MotionEvent的一個方法:getAction

這個方法返回一個整型變量,他的低1-8位表示該事件的類型,高9-16位表示觸控點索引。咱們只須要將這16位進行分離,就能夠知道觸控點的類型和所對應的觸控點。同時,MotionEvent有兩個獲取觸控點座標的方法:getX()/getY() ,他們都須要傳入一個觸控點索引來表示獲取哪一個觸控點的座標信息。

同時還要注意的是,MOVE事件和CANCEL事件是沒有包含觸控點索引的,只有DOWN類型和UP類型的事件才包含觸控點索引。這裏是由於非DOWN/UP事件,不涉及到觸控點的增長與刪除。

這裏咱們再來小結一下:

  • 一個MotionEvent對象內部使用一個數組來維護全部觸控點的信息
  • UP/DOWN類型的事件包含了觸控點索引,能夠根據該索引作出對應的操做
  • 觸控點的索引是變化的,不能做爲跟蹤的依據,而必須依據觸控點id

關於MotionEvent須要瞭解一個更加劇要的點:事件分離。

首先須要知道事件分發的一個原則:一個view消費了某一個觸點的down事件後,該觸點事件序列的後續事件,都由該view消費 。這也比較符合咱們的操做習慣。當咱們按下一個控件後,只要咱們的手指一直沒有離開屏幕,那麼咱們但願這個手指滑動的信息都交給這個view來處理。換句話說,一個觸控點的事件序列,只能給一個view消費。

通過前面的描述咱們知道,一個事件是包含全部觸摸點的信息的。當viewGroup在派發事件時,每一個觸摸點的信息就須要分開分別發送給感興趣的view,這就是事件分離。

例如Button1接收了觸摸點a的down事件,Button2接收了觸摸點b的down事件,那麼當一個MotionEvent對象到來時,須要將他裏面的觸摸點信息,把觸摸點a的信息拆開發送給button1,把觸摸點b的信息拆開發送給button2。以下圖:

事件分離

那麼,可不能夠不進行分離?固然能夠。這樣的話每次都把全部觸控點的信息發送給子view。這能夠經過FLAG_SPLIT_MOTION_EVENTS這個標誌進行設置是否要進行分離。

小結一下:

一個觸控點的序列通常狀況下只給一個view處理,當一個view消費了一個觸控點的down事件後,該觸控點的事件序列後續事件都會交給他處理。

事件分離是把一個motionEvent中的觸控點信息進行分離,只向子view發送其感興趣的觸控點信息。

咱們能夠經過設置FLAG_SPLIT_MOTION_EVENTS標誌讓viewGroup是否對事件進行分離


到這裏關於MotionEvent的內容就講得差很少,固然在分離的時候,還須要進行必定的調整,例如座標軸的更改、事件類型的更改等等,放在後面講,接下來看看ViewGroup是如何分發事件的。

ViewGroup對於事件的分發

這一步能夠說是事件分發中的重頭戲了。不過在理解了上面的MotionEvent以後,對於ViewGroup的分發細節也就容易理解了。

總體來講,ViewGroup分發事件分爲三個大部分,後面的內容也會圍繞着三大部分展開:

  1. 攔截事件:在必定狀況下,viewGroup有權利選擇攔截事件或者交給子view處理
  2. 尋找接收事件序列的控件:每個須要分發給子view的down事件都會先尋找是否有適合的子view,讓子view來消費整個事件序列
  3. 派發事件:把事件分發到感興趣的子view中或本身處理

大致的流程是:每個事件viewGroup會先判斷是否要攔截,若是是down事件(這裏的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),還須要挨個遍歷子view看看是否有子view消費了down事件,最後再把事件派發下去。

在開始解析以前,必須先了解一個關鍵對象:TouchTarget。

TouchTarget

前面咱們講到:一個觸控點的序列通常狀況下只給一個view處理,當一個view消費了一個觸控點的down事件後,該觸控點的事件序列後續事件都會交給他處理。對於viewGroup來講,他有不少個子view,若是不一樣的子view接受了不一樣的觸控點的down事件,那麼ViewGroup如何記錄這些信息並精準把事件發送給對應的子view呢?答案就是:TouchTarget。

TouchTarget中維護了每一個子view以及所對應的觸控點id,這裏的id能夠不止一個。TouchTarget自己是個鏈表,每一個節點記錄了子view所對應的觸控點id。在viewGroup中,該鏈表的鏈表頭是mFirstTouchTarget,若是他爲null,表示沒有任何子view接收了down事件。

TouchTarget有個很是神奇的設計,他只使用一個整型變量來記錄全部的觸控id。整型變量中哪個二進制位爲1,則對應綁定該id的觸控點。

例如 00000000 00000000 00000000 10001000,則表示綁定了id爲3和id爲7的兩個觸控點,由於第3位和第7位的二進制位是1。這裏能夠間接說明系統支持的最大多點觸控數是32,固然實際上通常是8比較多。當要判斷一個TouchTarget綁定了哪些id時,只須要經過必定的位操做便可,既提升了速度,也優化了空間佔用。

當一個down事件來臨時,viewGroup會爲這個down事件尋找適合的子view,併爲他們建立一個TouchTarget加入到鏈表中。而當一個up事件來臨時,viewGroup會把對應的TouchTarget節點信息刪除。那接下來,就直接看到viewGroup中的dispatchTouchEvent 是如何分發事件的。首先看到源碼中的第一部分:事件攔截。


事件攔截

這裏的攔截分爲兩部分:安全攔截和邏輯攔截。

安全攔截是一直被忽略的一種狀況。當一個控件a被另外一個非全屏控件b遮擋住的時候,那麼有可能被惡意軟件操做發生危險。例如咱們看到的界面是這樣的:

但實際上,咱們看到的這個按鈕時不可點擊的,實際上觸摸事件會被分發到這個按鈕後面的真正接收事件的按鈕:

而後咱們就白給了。這個安全攔截行爲由兩個標誌控制:

  • FILTER_TOUCHES_WHEN_OBSCURED:這個標誌能夠手動給控件設置,表示被非全屏控件覆蓋時,直接過濾掉全部觸摸事件。
  • FLAG_WINDOW_IS_OBSCURED:這個標誌表示當前窗口被一個非全屏控件覆蓋。

具體的源碼以下:

View.java api29
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    // 兩個標誌,前者表示當被覆蓋時不處理;後者表示當前窗口是否被非全屏窗口覆蓋
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

第二種攔截是邏輯攔截。若是當前viewGroup中沒有TouchTarget,並且這個事件不是down事件,這就意味着viewGroup本身消費了先前的down事件,那麼這個事件就無須分發到子view必須本身消費,也就不須要攔截這種狀況的事件。除此以外的事件都是須要分發到子view,那麼viewGroup就能夠對他們進行判斷是否進行攔截。簡單來講,只有須要分發到子view的事件才須要攔截

判斷是否攔截主要依靠兩個因素:FLAG_DISALLOW_INTERCEPT標誌和 onInterceptTouchEvent() 方法。

  1. 子view能夠經過requestDisallowInterupt方法強制要求viewGroup不要攔截事件,viewGroup中會設置一個FLAG_DISALLOW_INTERCEPT標誌表示不攔截事件。可是當前事件序列結束後,這個標誌會被清除。若是須要的話須要再次調用requestDisallowInterupt方法進行設置。
  2. 若是子view沒有強制要求不攔截,那麼會調用onInterceptTouchEvent() 方法判斷是否須要攔截。onInterceptTouchEvent方法默認只對一種特殊狀況做了攔截。通常狀況下咱們會重寫這個方法來攔截事件:
// 只對一種特殊狀況作了攔截
// 鼠標左鍵點擊了滑動塊
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

viewGroup的 dispatchTouchEvent 方法邏輯中對於事件攔截部分的源碼分析以下:

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
        
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        ...

        // 判斷是否須要攔截
        final boolean intercepted;
        // down事件或者有target的非down事件則須要判斷是否須要攔截
        // 不然不須要進行攔截判斷,由於必定是交給本身處理
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            // 此標誌爲子view經過requestDisallowInterupt方法設置
            // 禁止viewGroup攔截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 調用onInterceptTouchEvent判斷是否須要攔截
                intercepted = onInterceptTouchEvent(ev);
                // 恢復事件狀態
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 本身消費了down事件,那麼後續的事件非down事件都是本身處理
            intercepted = true;
        }
        ...;
    }
    ...;
}

尋找消費down事件的子控件

對於每個down事件,無論是ACTION_DOWN仍是ACTION_POINTER_DOWN,viewGroup都會優先在控件樹中尋找合適的子控件來消費他。由於對於每個down事件,標誌着一個觸控點的一個嶄新的事件序列,viewGroup會盡本身的最大能力尋找合適的子控件。若是找不到合適的子控件,纔會本身處理down事件。由於,消費了down事件,意味着接下來該觸控點的事件序列事件都會交給該view消費,若是viewGroup攔截了事件,那麼子view就沒法接收到任何事件消息。

viewGroup尋找子控件的步驟也不復雜。首先viewGroup會爲他的子控件構造一個控件列表,構造的順序是view的繪製順序的逆序,也就是一個view的z軸系數越高,顯示高度越高,在列表的順序就會越靠前。這其實比較好理解,顯示越高的控件確定是優先接收點擊的。除了默認狀況,咱們也能夠進行自定義列表順序,這裏就不展開了。

viewGroup會按順序遍歷整個列表,判斷觸控點的位置是否在該view的範圍內、該view是否能夠點擊等,尋找合適的子view。若是找到合適的子view,則會把down事件分發給他,若是該view接收事件,則會爲他建立一個TouchTarget,將該觸控id和view進行綁定,以後該觸控點的事件就能夠直接分發給他了。

而若是沒有一個控件適合,那麼會默認選取TouchTarget鏈表的最新一個節點。也就是當咱們多點觸控時,兩次手指按下,若是沒有找到合適的子view,那麼就被認爲是和上一個手指點擊的是同個view。所以,若是viewGroup當前有正在消費事件的子控件,那麼viewGroup本身是不會消費down事件的。

接下來咱們看看源碼分析(代碼有點長,須要慢慢分析理解):

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
         
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        // action的高9-16位表示索引值
        // 低1-8位表示事件類型
        // 只有down或者up事件纔有索引值
        final int action = ev.getAction();
        // 獲取到真正的事件類型
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        ...

        // 攔截內容的邏輯
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            ...
        } 

        ...

        // 三個變量:
        // split表示是否須要對事件進行分裂,對應多點觸摸事件
        // newTouchTarget 若是是down或pointer_down事件的新的綁定target
        // alreadyDispatchedToNewTouchTarget 表示事件是否已經分發給targetview了
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        // 若是沒有取消和攔截進入分發
        if (!canceled && !intercepted) {
			...
			// down或pointer_down事件,表示新的手指按下了,須要尋找接收事件的view
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                
                // 多點觸控會有不一樣的索引,獲取索引號
                // 該索引位於MotionEvent中的一個數組,索引值就是數組下標值
                // 只有up或down事件纔會攜帶索引值
                final int actionIndex = ev.getActionIndex(); 
                // 這個整型變量記錄了TouchTarget中view所對應的觸控點id
                // 觸控點id的範圍是0-31,整型變量中哪個二進制位爲1,則對應綁定該id的觸控點
                // 例如 00000000 00000000 00000000 10001000
                // 則表示綁定了id爲3和id爲7的兩個觸控點
                // 這裏根據是否須要分離,對觸控點id進行記錄,
                // 而若是不須要分離,則默認接收全部觸控點的事件
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

                // down事件表示該觸控點事件序列是一個新的序列
                // 清除以前綁定到到該觸控id的TouchTarget
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                // 若是子控件數目不爲0並且還沒綁定到新的id
                if (newTouchTarget == null && childrenCount != 0) {
                    // 使用觸控點索引獲取觸控點位置
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 從前到後建立view列表
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    // 判斷是不是自定義view順序
                    final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    
                    // 遍歷全部子控件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 從子控件列表中獲取到子控件
                        final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                        
                        ...

                        // 檢查該子view是否能夠接受觸摸事件和是否在點擊的範圍內
                        if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 檢查該子view是否在touchTarget鏈表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 鏈表中已經存在該子view,說明這是一個多點觸摸事件
                            // 即兩次都觸摸到同一個view上
                            // 將新的觸控點id綁定到該TouchTarget上
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        // 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
                        // 若是消費了,須要生成新的TouchTarget
                        // 若是沒有消費,說明子view不接受該down事件,繼續循環尋找合適的子控件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 保存該觸控事件的相關信息
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 保存該view到target鏈表
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 標記已經分發給子view,退出循環
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ...
                    }// 這裏對應for (int i = childrenCount - 1; i >= 0; i--)
                    ...
                }// 這裏對應判斷:(newTouchTarget == null && childrenCount != 0)

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有子view接收down事件,直接選擇鏈表尾的view做爲target
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
                
            }// 這裏對應if (actionMasked == MotionEvent.ACTION_DOWN...)
        }// 這裏對應if (!canceled && !intercepted)
        ...
    }// 這裏對應if (onFilterTouchEventForSecurity(ev))
    ...
}

派發事件

通過了攔截與尋找消費down事件的控件以後,不管前面的處理結果如何,最終都是須要將事件進行派發,無論是派發給本身仍是子控件。這裏派發的對象只有兩個:viewGroup自身或TouchTarget。

通過了前面的尋找消費down事件子控件步驟,那麼每一個觸控點都找到了消費本身事件序列的控件並綁定在了TouchTarget中;而若是沒有找到合適的子控件,那麼消費的對象就是viewGroup本身。所以派發事件的主要任務就是:把不一樣觸控點的信息分發給合適的viewGroup或touchTarget。

派發的邏輯須要結合前面MotionEvent和TouchTarget的內容。咱們知道MotionEvent包含了當前屏幕全部觸控點信息,而viewGroup的每一個TouchTarget則包含了不一樣的view所感興趣的觸控點。
若是不須要進行事件分離,那麼直接將當前的全部觸控點的信息都發送給每一個TouchTarget便可;
若是須要進行事件分離,那麼會將MotionEvent中不一樣觸控點的信息拆開分別建立新的MotionEvent,併發送給感興趣的子控件;
若是TouchTarget鏈表爲空,那麼直接分發給viewGroup本身;因此touchTarget不爲空的狀況下,viewGroup本身是不會消費事件的,這也就意味着viewGroup和其中的view不會同時消費事件。

事件分離派發事件

上圖展現了須要事件分離的狀況下進行的事件分發。

在把原MotionEvent拆分紅多個MotionEvent時,不只須要把不一樣的觸控點信息進行分離,還須要對座標進行轉換和改變事件類型:

  • 咱們接收到的觸控點的位置信息並非基於屏幕座標系,而是基於當前view的座標系。因此當viewGroup往子view分發事件時,須要把觸控點的信息轉換成對應view的座標系。
  • viewGroup收到的事件類型和子view收到的事件類型並非徹底一致的,在分發給子view的時候,viewGroup須要對事件類型進行修改,通常有如下狀況須要修改:
    1. viewGroup收到一個ACTION_POINTER_DOWN事件分發給一個子view,可是該子view前面沒有收到其餘的down事件,因此對於該view來講這是一個嶄新的事件序列,因此須要把這個ACTION_POINTER_DOWN事件類型改成ACTION_DOWN再發送給子view。
    2. viewGroup收到一個ACTION_POINTER_DOWN或ACTION_POINTER_UP事件,假設這個事件類型對應觸控點2,可是有一個子view他只對觸控點1的事件序列感興趣,那麼在分離出觸控點1的信息以後,還須要把事件類型改成ACTION_MOVE再分發給該子view。
  • 注意,把原MotionEvent對象拆分爲多個MotionEvent對象以後,觸控點的索引也發生了改變,若是須要分發一個ACTION_POINTER_DOWN/UP事件給子view,那麼須要注意更新觸控點的索引值。

viewGroup中真正執行事件派發的關鍵方法是 dispatchTransformedTouchEvent ,該方法會完成關鍵的事件分發邏輯。源碼分析以下:

ViewGroup.java api29
// 該方法接收原MotionEvent事件、是否進行取消、目標子view、以及目標子view感興趣的觸控id
// 若是不是取消事件這個方法會把原MotionEvent中的觸控點信息拆分出目標view感興趣的觸控點信息
// 若是是取消事件則不須要拆分直接發送取消事件便可
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // 若是是取消事件,那麼不須要作其餘額外的操做,直接派發事件便可,而後直接返回
    // 由於對於取消事件最重要的內容就是事件自己,無需對事件的內容進行設置
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // oldPointerIdBits表示如今全部的觸控id
    // desirePointerIdBits來自於該view所在的touchTarget,表示該view感興趣的觸控點id
    // 由於desirePointerIdBits有可能全是1,因此須要和oldPointerIdBits進行位與
    // 獲得真正可接收的觸控點信息
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // 控件處於不一致的狀態。正在接受事件序列卻沒有一個觸控點id符合
    if (newPointerIdBits == 0) {
        return false;
    }

    // 來自原始MotionEvent的新的MotionEvent,只包含目標感興趣的觸控點
    // 最終派發的是這個MotionEvent
    final MotionEvent transformedEvent;
    
    // 二者相等,表示該view接受全部的觸控點的事件
    // 這個時候transformedEvent至關於原始MotionEvent的複製
    if (newPointerIdBits == oldPointerIdBits) {
        // 當目標控件不存在經過setScaleX()等方法進行的變換時,
        // 爲了效率會將原始事件簡單地進行控件位置與滾動量變換以後
        // 發送給目標的dispatchTouchEvent()方法並返回。
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        // 複製原始MotionEvent
        transformedEvent = MotionEvent.obtain(event);
    } else {
        // 若是二者不等,說明須要對事件進行拆分
        // 只生成目標感興趣的觸控點的信息
        // 這裏分離事件包括了修改事件的類型、觸控點索引等
        transformedEvent = event.split(newPointerIdBits);
    }

    // 對MotionEvent的座標系,轉換爲目標控件的座標系並進行分發
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        // 計算滾動量偏移
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        // 存在scale等變換,須要進行矩陣轉換
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
		// 調用子view的方法進行分發
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // 分發完畢,回收MotionEvent
    transformedEvent.recycle();
    return handled;
}

好了,瞭解完上面的內容,來看看viewGroup的 dispatchTouchEvent 中派發事件的代碼部分:

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
        
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
		...

		
        if (mFirstTouchTarget == null) {
            // 通過了前面的處理,到這裏touchTarget依舊爲null,說明沒有找處處理down事件的子控件
            // 或者down事件被viewGroup自己消費了,因此該事件由viewGroup本身處理
            // 這裏調用了dispatchTransformedTouchEvent方法來分發事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 已經有子view消費了down事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷全部的TouchTarget並把事件分發下去
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // 表示事件在前面已經處理了,不須要重複處理
                    handled = true;
                } else {
                    // 正常分發事件或者分發取消事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                    // 這裏調用了dispatchTransformedTouchEvent方法來分發事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                      target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 若是發送了取消事件,則移除該target
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 若是接收到取消獲取up事件,說明事件序列結束
        // 直接刪除全部的TouchTarget
        if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            // 清除記錄的信息
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            // 若是僅僅只是一個PONITER_UP
            // 清除對應觸控點的觸摸信息
            removePointersFromTouchTargets(idBitsToRemove);
        }
        
    }// 這裏對應if (onFilterTouchEventForSecurity(ev))

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

小結

到這裏,viewGroup的事件分發源碼就解析完成了,這裏再來小結一下:

  • 每個觸控點的事件序列,只能給一個view消費;若是一個view消費了一個觸控點的down事件,那麼該觸控點的後續事件都會給他處理。
  • 每個事件到達viewGroup,若是須要分發到子view,那麼viewGroup會新判斷是否要攔截。
    • 當viewGroup的touchTarget!=null || 事件的類型爲down 須要進行判斷是否攔截;
    • 判斷是否攔截受兩個因素影響:onInterceptTouchEvent和FLAG_DISALLOW_INTERCEPT標誌
  • 若是該事件是down類型,那麼須要遍歷全部的子控件判斷是否有子控件消費該down事件
    • 當有新的down事件被消費時,viewGroup會把該view和對應的觸控點id綁定起來存儲到touchTarget中
  • 根據前面的處理狀況,將事件派發到viewGroup自身或touchTarget中
    • 若是touchTarget==null,說明沒有子控件消費了down事件,那麼viewGroup本身處理事件
    • 不然將事件分離成多個MotionEvent,每一個MotionEvent只包含對應view感興趣的觸控點的信息,並派發給對應的子view

viewGroup中的源碼不少,但大致的邏輯也就這三大部分。理解好MotionEvent和TouchTarget的設計,那麼理解viewGroup的事件分發源碼也是手到擒來。上面的源碼我省略了一些細節內容,下面附上完整的viewGroup分發代碼。

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一致性檢驗器,用於調試用途
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
        
    // 輔助功能,用於輔助有障礙人羣使用;
    // 若是這個事件是輔助功能事件,那麼他會帶有一個target view,要求事件必須分發給該view
    // 若是setTargetAccessibilityFocus(false),表示取消輔助功能事件,按照常規的事件分發進行
    // 這裏表示若是當前是目標target view,則取消標誌,直接按照普通分發便可
    // 後面還有不少相似的代碼,都是一樣的道理
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }   

    boolean handled = false;
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        // action的高9-16位表示索引值
        // 低1-8位表示事件類型
        // 只有down或者up事件纔有索引值
        final int action = ev.getAction();
        // 獲取到真正的事件類型
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // ACTION_DOWN事件,表示這是一個全新的事件序列,會清除全部的touchTarget,重置全部狀態
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 判斷是否須要攔截
        final boolean intercepted;
        // down事件或者有target的非down事件則須要判斷是否須要攔截
        // 不然直接攔截本身處理
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            // 此標誌爲子view經過requestDisallowInterupt方法設置
            // 禁止viewGroup攔截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 調用onInterceptTouchEvent判斷是否須要攔截
                intercepted = onInterceptTouchEvent(ev);
                // 恢復事件狀態
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 本身消費了down事件
            intercepted = true;
        }

        // 若是已經被攔截、或者已經有了目標view,取消輔助功能的target標誌
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // 判斷是否須要取消
        // 這裏有不少種狀況須要發送取消事件
        // 最多見的是viewGroup攔截了子view的ACTION_MOVE事件,致使事件序列中斷
        // 那麼須要發送cancel事件告知該view,讓該view作一些狀態恢復工做
        final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

        // 三個變量:
        // 是否須要對事件進行分裂,對應多點觸摸事件
        // newTouchTarget 若是是down或pointer_down事件的新的綁定target
        // alreadyDispatchedToNewTouchTarget 是否已經分發給target view了
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        // 下面部分的代碼是尋找消費down事件的子控件
        // 若是沒有取消和攔截進入分發
        if (!canceled && !intercepted) {
			// 若是是輔助功能事件,咱們會尋找他的target view來接收這個事件
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            
			// down或pointer_down事件,表示新的手指按下了,須要尋找接收事件的view
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                
                // 多點觸控會有不一樣的索引,獲取索引號
                // 該索引位於MotionEvent中的一個數組,索引值就是數組下標值
                // 只有up或down事件纔會攜帶索引值
                final int actionIndex = ev.getActionIndex(); 
                
                // 這個整型變量記錄了TouchTarget中view所對應的觸控點id
                // 觸控點id的範圍是0-31,整型變量中哪個二進制位爲1,則對應綁定該id的觸控點
                // 例如 00000000 00000000 00000000 10001000
                // 則表示綁定了id爲3和id爲7的兩個觸控點
                // 這裏根據是否須要分離,對觸控點id進行記錄,
                // 而若是不須要分離,則默認接收全部觸控點的事件
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

                // 清除以前獲取到該觸控id的TouchTarget
                removePointersFromTouchTargets(idBitsToAssign);

                // 若是子控件的數量等於0,那麼不須要進行遍歷只能給viewGroup本身處理
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    // 使用觸控點索引獲取觸控點位置
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 從前到後建立view列表
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    // 這一句判斷是不是自定義view順序
                    final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    
                     // 遍歷全部子控件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 得到真正的索引和子view
                        final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);

                        // 若是是輔助功能事件,則優先給對應的target先處理
                        // 若是該view不處理,再交給其餘的view處理
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        // 檢查該子view是否能夠接受觸摸事件和是否在點擊的範圍內
                        if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 檢查該子view是否在touchTarget鏈表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 鏈表中已經存在該子view,說明這是一個多點觸摸事件
                            // 將新的觸控點id綁定到該TouchTarget上
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
						
                        // 設置取消標誌
                        // 下一次再次調用這個方法就會返回true
                        resetCancelNextUpFlag(child);
                        
                        // 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
                        // 若是消費了,須要生成新的TouchTarget
                        // 若是沒有消費,說明子view不接受該down事件,繼續循環尋找合適的子控件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 保存信息
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 保存該view到target鏈表
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 標記已經分發給子view,退出循環
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // 輔助功能事件對應的targetView沒有消費該事件,則繼續分發給普通view
                        ev.setTargetAccessibilityFocus(false);
                        
                    }// 這裏對應for (int i = childrenCount - 1; i >= 0; i--)
                    
                    if (preorderedList != null) preorderedList.clear();
                    
                }// 這裏對應判斷:(newTouchTarget == null && childrenCount != 0)

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有子view接收down事件,直接選擇鏈表尾的view做爲target
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }// 這裏對應if (actionMasked == MotionEvent.ACTION_DOWN...)
        }// 這裏對應if (!canceled && !intercepted)

        if (mFirstTouchTarget == null) {
            // 通過了前面的處理,到這裏touchTarget依舊爲null,說明沒有找處處理down事件的子控件
            // 或者down事件被viewGroup自己消費了,因此該事件由viewGroup本身處理
            // 這裏調用了dispatchTransformedTouchEvent方法來分發事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 已經有子view消費了down事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷全部的TouchTarget並把事件分發下去
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // 表示事件在前面已經處理了,不須要重複處理
                    handled = true;
                } else {
                    // 正常分發事件或者分發取消事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                    // 這裏調用了dispatchTransformedTouchEvent方法來分發事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                      target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 若是發送了取消事件,則移除該target
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 若是接收到取消獲取up事件,說明事件序列結束
        // 直接刪除全部的TouchTarget
        if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            // 清除記錄的信息
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            // 若是僅僅只是一個PONITER_UP
            // 清除對應觸控點的觸摸信息
            removePointersFromTouchTargets(idBitsToRemove);
        }
        
    }// 這裏對應if (onFilterTouchEventForSecurity(ev))

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

View對於事件的分發

無論是viewGroup本身處理事件,仍是view處理事件,若是沒有被子類攔截(子類重寫方法),最終都會調用到 view.dispatchTouchEvent 方法來處理事件。view處理事件的邏輯就比viewGroup簡單多了,由於它不須要向下去分發事件,只須要本身處理。總體的邏輯以下:

  1. 首先判斷是否被其餘非全屏view覆蓋。這和上面viewGroup的安全性檢查是同樣的
  2. 通過檢查以後先檢查是否有onTouchListener監聽器,若是有則調用它
  3. 若是第2步沒有消費事件,那麼會調用onTouchEvent方法來處理事件
    • 這個方法是view處理事件的核心,裏面包含了點擊、雙擊、長按等邏輯的處理須要重點關注。

咱們先看到 view.dispatchTouchEvent 方法源碼:

View.java api29
public boolean dispatchTouchEvent(MotionEvent event) {
    // 首先處理輔助功能事件
    if (event.isTargetAccessibilityFocus()) {
        // 本控件沒有獲取到焦點,不處理事件
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // 獲取到焦點,按照常規處理事件
        event.setTargetAccessibilityFocus(false);
    }

    // 表示是否消費事件
    boolean result = false;

    // 一致性檢驗器,檢驗事件是否一致
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    } 

    // 若是是down事件,中止嵌套滑動
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    // 安全過濾,本窗口位於非全屏窗口之下時,可能會阻止控件處理觸摸事件
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            // 若是事件爲鼠標拖動滾動條
            result = true;
        }
        // 先調用onTouchListener監聽器
        // 當咱們設置onTouchEventListener以後,L
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 若onTouchListener沒有消費事件,調用onTouchEvent方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    // 一致性檢驗
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // 若是是事件序列終止事件或者沒有消費down事件,終止嵌套滑動
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

源碼內容不長,主要的邏輯內容上面已經講了,其餘的都是一些細節的處理。onTouchListener通常狀況下咱們是不會使用,那麼接下來咱們直接看到onTouchEvent方法。

onTouchEvent整體上就作一件事:根據按下狀況選擇觸發onClickListener或者onLongClickListener ,也就是判斷是單擊仍是長按事件,其餘的源碼都是實現細節。onTouchEvent方法正確處理每個事件類型,來確保點擊與長按監聽器能夠被準確地執行。理解onTouchEvent的源碼以前,有幾個重要的點須要先了解一下。

咱們的操做模式有按鍵模式、觸摸模式。按鍵模式對應的是外接鍵盤或者之前的老式鍵盤機,在按鍵模式下咱們要點擊一個按鈕一般都是先使用方向光標選中一個button(也就是讓該button獲取到focus),而後再點擊確認按下一個button。可是在觸摸模式下,button卻不須要獲取焦點。若是一個view在觸摸模式下能夠獲取焦點,那麼他將沒法響應點擊事件,也就是沒法調用onClickListener監聽器 ,例如EditText。

view辨別單擊和長按的方法是設置延時任務,在源碼中會看到不少的相似的代碼,這裏延時任務使用handler來實現。當一個down事件來臨時,會添加一個延時任務到消息隊列中。若是時間到尚未接收到up事件,說明這是個長按事件,那麼就會調用onLongClickListener監聽器,而若是在延時時間內收到了up事件,那麼說明這是個單擊事件,取消這個延時的任務,並調用onClickListener。判斷是不是一個長按事件,調用的是 checkForLongClick 方法來設置延時任務:

// 接收四個參數:
// delay:延時的時長;x、y: 觸控點的位置;classification:長按類型分類
private void checkForLongClick(long delay, float x, float y, int classification) {
    // 只有是能夠長按或者長按會顯示工具提示的view纔會建立延時任務
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        // 標誌還沒觸發長按
        // 若是延遲時間到,觸發長按監聽,這個變量 就會被設置爲true
        // 那麼當up事件到來時,就不會觸摸單擊監聽,也就是onClickListener
        mHasPerformedLongPress = false;

        // 建立CheckForLongPress
        // 這是一個實現Runnable接口的類,run方法中回調了onLongClickListener
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        // 設置參數
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        mPendingCheckForLongPress.setClassification(classification);
        // 使用handler發送延時任務
        postDelayed(mPendingCheckForLongPress, delay);
    }
}

上面這個方法的邏輯仍是比較簡單的,下面看看 CheckForLongPress 這個類:

private final class CheckForLongPress implements Runnable {
...
    @Override
    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            // 在延時時間到以後,就會運行這個任務
            // 調用onLongClickListener監聽器
            // 並設置mHasPerformedLongPress爲true
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
    }
...
}

延遲時間結束後,就會運行 CheckForLongPress 對象,回調onLongClickListener,這樣就表示這是一個長按的事件了。

另外,在默認的狀況下,當咱們按住一個view,而後手指滑動到該view所在的範圍以外,那麼系統會認爲你對這個view已經不感興趣,因此沒法觸發單擊和長按事件。固然,不少時候並非如此,這就須要具體的view來重寫onTouchEvent邏輯了,可是view的默認實現是這樣的邏輯。

好了,那麼接下來就來看一下完整的 view.onTouchEvent 代碼:

View.java api29
public boolean onTouchEvent(MotionEvent event) {
    // 獲取觸控點座標
    // 這裏咱們發現他是沒有傳入觸控點索引的
    // 因此默認狀況下view是隻處理索引爲0的觸控點
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    // 判斷是不是可點擊的
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // 一個被禁用的view若是被設置爲clickable,那麼他仍舊是能夠消費事件的
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            // 若是是按下狀態,取消按下狀態
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // 返回是否能夠消費事件
        return clickable;
    }
    
    // 若是設置了觸摸事件代理你,那麼直接調用代理來處理事件
    // 若是代理消費了事件則返回true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    // 若是該控件是可點擊的,或者長按會出現工具提示
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // 若是是長按顯示工具類標誌,回調該方法
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                // 若是是不可點擊的view,同時會清除全部的標誌,恢復狀態
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                
                // 判斷是不是按下狀態
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // 若是能夠獲取焦點可是沒有得到焦點,請求獲取焦點
                    // 正常的觸摸模式下是不須要獲取焦點,例如咱們的button
                    // 可是若是在按鍵模式下,須要先移動光標選中按鈕,也就是獲取focus
                    // 再點擊確認觸摸按鈕事件
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // 確保用戶看到按下狀態
                        setPressed(true, x, y);
                    }

                    // 兩個參數分別是:長按事件是否已經響應、是否忽略本次up事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 這是一個單擊事件,還沒到達長按的時間,移除長按標誌
                        removeLongPressCallback();

                        // 只有不能獲取焦點的控件才能觸摸click監聽
                        if (!focusTaken) {
                            // 這裏使用發送到消息隊列的方式而不是當即執行onClickListener
                            // 緣由在於能夠在點擊前觸發一些其餘視覺效果
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }

                    // 取消按下狀態
                    // 這裏也是個post任務
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // 若是發送到隊列失敗,則直接取消
                        mUnsetPressedState.run();
                    }

                    // 移除單擊標誌
                    removeTapCallback();
                }
                // 忽略下次up事件標誌設置爲false
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                // 輸入設備源是不是可觸摸屏幕
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                // 標誌是不是長按
                mHasPerformedLongPress = false;

                // 若是是不可點擊的view,說明是長按提示工具的view
                // 直接檢查是否發生了長按
                if (!clickable) {
                    // 這個方法會發送一個延遲的任務
                    // 若是延遲時間到仍是按下狀態,那麼就會回調onLongClickListener接口
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    break;
                }

                // 判斷是不是鼠標右鍵或者手寫筆的第一個按鈕
                // 特殊處理直接返回
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // 向上遍歷view查看是否在一個可滑動的容器中
                boolean isInScrollingContainer = isInScrollingContainer();

                // 若是在一個可滑動的容器中,那麼須要延遲一小會再響應反饋
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    // 利用消息隊列來延遲檢測一個單擊事件,延遲時間是ViewConfiguration.getTapTimeout()
                    // 這個時間是100ms
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // 沒有在可滑動的容器中,直接響應觸摸反饋
                    // 設置按下狀態爲true
                    setPressed(true, x, y);
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                // 取消事件,恢復全部的狀態
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                // 通知view和drawable熱點改變
                // 暫時不知道什麼意思
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                final int motionClassification = event.getClassification();
                final boolean ambiguousGesture =
                        motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                int touchSlop = mTouchSlop;
                
                // view已經被設置了長按標誌且目前的事件標誌是模糊標誌
                // 系統並不知道用戶的意圖,因此即便滑出了view的範圍,並不會取消長按標誌
                // 而是延長越界的偏差範圍和檢查長按的時間
                // 由於這個時候系統並不知道你是想要長按仍是要滑動,結果就是兩種行爲都沒有響應
                // 由你接下來的行爲決定
                if (ambiguousGesture && hasPendingLongPressCallback()) {
                    final float ambiguousMultiplier =
                            ViewConfiguration.getAmbiguousGestureMultiplier();
                    // 判斷此時觸控點的位置是否還在view的範圍內
                    // touchSlop是一個小範圍的偏差,超出view位置slop距離依舊斷定爲在view範圍內
                    if (!pointInView(x, y, touchSlop)) {
                       // 移除原來的長按標誌
                        removeLongPressCallback();
                        // 延長等待時間,這裏是原來長按等待的兩倍
                        long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                * ambiguousMultiplier);
                        // 減去已經等待的時間
                        delay -= event.getEventTime() - event.getDownTime();
                        // 添加新的長按標誌
                        checkForLongClick(
                                delay,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    touchSlop *= ambiguousMultiplier;
                }

                // 判斷此時觸控點的位置是否還在view的範圍內
                // touchSlop是一個小範圍的偏差,超出view位置slop距離依舊斷定爲在view範圍內
                if (!pointInView(x, y, touchSlop)) {
                    // 若是已經超出範圍,直接移除點擊標誌和長按標誌,點擊和長按事件均沒法響應
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // 取消按下標誌
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }

                final boolean deepPress =
                        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                // 表示用戶在屏幕上用力按壓,加快長按響應速度
                if (deepPress && hasPendingLongPressCallback()) {
                    // 移除原來的長按標誌,直接響應長按事件
                    removeLongPressCallback();
                    checkForLongClick(
                            0 /* 延遲時間爲0 */,
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                }
                break;
        }

        return true;
    } // 對應if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) 

    return false;
}

最後

若是你能看到這裏,說明你對於viewGroup和view的事件處理源碼已經瞭如指掌了。(高興之餘不如給筆者點個贊?(: ~)

最後這裏再來總結一下:

  • 觸摸事件,從屏幕產生後,通過系統服務的處理,最終會發送到viewRootImpl來進行分發;
  • viewRootImpl會調用它所管理的view的 dispatchTouchEvent 方法來分發事件,那麼這裏就會分爲兩種狀況:
    1. 若是是view,那麼會直接處理事件
    2. 若是是viewGroup,那麼會向下派發事件
  • viewGroup會爲每一個觸控點儘可能尋找感興趣的子view,最後再本身處理事件。viewGroup的任務就是把事件分發按照原則精準地分發給他子view。
    • 事件分發中一個很是重要的原則就是:一個觸控點的事件序列,只能給一個view消費,除了特殊狀況,如被viewGroup攔截。
    • viewGroup爲了踐行這個原則,touchTarget的設計是很是重要的;他將view與觸控點進行綁定,讓一個觸控點的事件只會給一個view消費
  • view的 dispatchTouchEvent 主要內容是處理事件。首先會調用onTouchListener,若是其沒有處理則會調用onTouchEvent方法。
    • onTouchEvent的默認實現中的主要任務就是辨別單擊與長按事件,並回調onClickListener與onLongClickListener

到此本文的內容就結束了,事件分發的總體流程回顧、學了事件分發有什麼做用、高頻面試題相關文章,將會在後續繼續創做。

原創不易,你的點贊是我最大的動力。感謝閱讀 ~

優秀文獻

在學習過程當中,如下相關資料給了我很是大的幫助,都是很是優秀的文章:

  • 《深刻理解android卷Ⅲ》:學習android系統必備,做者對於android系統的理解很是透徹,能夠幫助咱們認識到最本質的知識,而不是停留在表層。但對於新手可能會比較難以讀懂。
  • 《Android開發藝術探索》:進階學習android必備,做者講得比較通俗易懂。深度可能相對而言可能較淺,但對新手比較友好,例如筆者。
  • Android 觸摸事件分發機制(三)View觸摸事件分發機制 : 這篇文章採用拆分源碼的思路來說解源碼,更好地吸取源碼中的內容,筆者也是借鑑了他的寫法來創做本文。文中對於源碼的分析很是到位,值得一看。
  • 安卓自定義View進階-事件分發機制詳解 : 做者言語幽默,通俗易懂,不可多得的好文。
  • Android事件分發機制 詳解攻略,您值得擁有 : 著名博主carson_Ho的文章。特色是乾貨滿滿。全文無廢話,只講重要知識點,適合用來複習知識點。
  • Android事件分發機制 : gityuan大佬的博客,對於源碼的研究都很深刻。但對於一些源碼細節並無作過多的解釋,有些地方難以理解。

全文到此,原創不易,以爲有幫助能夠點贊收藏評論轉發。
筆者才疏學淺,有任何想法歡迎評論區交流指正。
如需轉載請評論區或私信交流。

另外歡迎光臨筆者的我的博客:傳送門

相關文章
相關標籤/搜索