前言
iOS事件的傳遞與響應是一個重要的話題,此文將結合蘋果官方的文檔對事件的傳遞與響應原理及應用實踐做一個比較完整的總結。文章將依次介紹下列內容:
事件的傳遞機制
事件的響應機制
事件傳遞與響應實踐
手勢識別器工作機制
標準控件的事件處理
iOS中事件一共有四種類型,包含觸摸事件,運動事件(加速器),遠程控制事件,按壓事件(3D touch),本文將只討論最常用的觸摸事件。事件通過UIEvent對象描述
UIEvent
UIEvent描述了單次的用戶與應用的交互行為,例如觸摸屏幕會產(chǎn)生觸摸事件,晃動手機會產(chǎn)生運動事件。UIEvent對象中記錄了事件發(fā)生的時間,類型,對于觸摸事件,還記錄了一組UITouch對象,下面是UIEvent的幾個屬性:

那么觸摸事件中的UITouch對象描述的是什么呢?
UITouch
UITouch記錄了手指在屏幕上觸摸時產(chǎn)生的一組信息,包含觸摸的時間,位置,所在的窗口或視圖,觸摸的狀態(tài),力度等信息

每一根手指的觸摸都會產(chǎn)生一個UITouch對象,多個手指觸摸便會有多個UITouch對象,當手指在屏幕上移動時,系統(tǒng)會更新UITouch的部分屬性值,在觸摸結束后系統(tǒng)會釋放UITouch對象。
當事件產(chǎn)生后,系統(tǒng)會尋找可以響應該事件的對象來處理事件,如果找不到可以響應的對象,事件就會被丟棄。那么哪些對象可以響應事件呢?只有繼承于UIResponder的對象才能夠響應事件,UIApplication,UIView,UIViewcontroller均繼承于UIResponder,因此它們均能夠響應事件。UIResponder提供了響應事件的一組方法:

如果我們想要對事件進行自定義的處理(比如手指在屏幕滑動時讓某個view跟著移動),我們需要重寫以上四個方法,對于UIViewcontroller,我們只需要在UIViewcontroller中重寫上面四個方法,對于UIView,我們需要創(chuàng)建繼承于UIView的子類,然后在子類中重寫上面的方法,這點需要注意
事件的傳遞
事件產(chǎn)生之后,會被加入到由UIApplication管理的事件隊列里,接下來開始自UIApplication往下傳遞,首先會傳遞給主window,然后按照view的層級結構一層層往下傳遞,一直找到最合適的view(發(fā)生touch的那個view)來處理事件。查找最合適的view的過程是一個遞歸的過程,其中涉及到兩個重要的方法 hitTest:withEvent:和pointInside:withEvent:
當事件傳遞給某個view之后,會調用view的hitTest:withEvent:方法,該方法會遞歸查找view的所有子view,其中是否有最合適的view來處理事件,整個流程如下所示:
hitTest:withEvent代碼實現(xiàn):

-
pointInside:withEvent:方法作用是判斷點是否在視圖內,是則返回YES,否則返回NO - 判斷一個view是否能夠接收事件有三個條件,分別是,是否禁止用戶交互(userInteractionEnabled = NO),是否被隱藏(hidden = YES)以及透明度是否小于等于0.01(alpha <=0.01)
- 從遞歸的邏輯我們知道,如果觸摸的點不在父view上,那么其上的所有子view的hitTest都不會被調用,需要指出的是,如果子view尺寸超出了父view,并且屬性clipsToBounds設置為NO(也就是子view超出部分不被裁剪),觸摸發(fā)生在子view超出父view的區(qū)域內,依舊不返回子view。反過來,如果觸摸的點在父view上并且父view就是最合適的view,那么它的所有子view的hitTest還是會被調用,因為如果不調用就無法知道是否還有比父view更合適的子view存在。
事件的響應
在找到最合適的view之后,會調用view的touches方法對事件進行響應,如果沒有重寫view的touches方法,touches默認的做法是將事件沿著響應者鏈往上拋,交給下一個響應者對象。也就是說,touches方法默認不處理事件,只是將事件沿著響應者鏈往上傳遞。那么響應者鏈是什么呢?
響應者鏈
在應用程序中,視圖放置都是有一定層次關系的,點擊屏幕之后該由下方的哪個view來響應需要有一個判斷的方式。響應者鏈是由一系列可以響應事件的對象(繼承于UIResponder)組成的,它決定了響應者對象響應事件的先后順序關系。下圖展示了UIApplication,UIViewcontroller以及UIView之間的響應關系鏈:

響應者鏈在遞歸查找最合適的view的時候形成,所找到的view將成為第一響應者,會調用它的touches方法來響應事件,touches方法默認的處理是將事件往上拋給下一個響應者,而如果下一個響應者的touches方法沒有重寫,事件會繼續(xù)沿著響應者鏈往上走,一直到UIApplication,如果依舊不能處理事件那么事件就被丟棄。
UIView
如果view是viewcontroller的根view,那么下一個響應者是viewcontroller,否則是super view
UIViewcontroller
如果viewcontroller的view是window的根view,那么下一個響應者是window;如果viewcontroller是另一個viewcontroller模態(tài)推出的,那么下一個響應者是另一個viewcontroller;如果viewcontroller的view被add到另一個viewcontroller的根view上,那么下一個響應者是另一個viewcontroller的根view
UIWindow
UIWindow的下一個響應者是UIApplication
UIApplication
通常UIApplication是響應者鏈的頂端(如果app delegate也繼承了UIResponder,事件還會繼續(xù)傳給app delegate)
事件傳遞與響應實踐
首先我們通過代碼創(chuàng)建一個具有層次結構的視圖集合,在viewcontroller的viewDidLoad中添加如下代碼:

執(zhí)行后如下所示:
要實現(xiàn)我們自定義的事件處理邏輯,通常有兩種方式,我們可以重寫hitTest:withEvent:方法指定最合適處理事件的視圖,即響應鏈的第一響應者,也可以通過重寫touches方法來決定該由響應鏈上的誰來響應事件。
-
情景1:點擊黃色視圖,紅色視圖響應
黃色視圖和紅色視圖均為綠色視圖的子視圖,我們可以重寫綠色視圖的hitTest:withEvent:方法,在其中直接返回紅色視圖,代碼示例如下:
777.png我們這里是重寫了父視圖的hitTest方法,而不是重寫紅色視圖的hitTest方法并讓它返回自身,道理也很顯然,在遍歷綠色視圖所有子視圖的過程中,可能還沒來得及調用到紅色視圖的hitTest方法時,就已經(jīng)遍歷到了觸摸點真正所在的黃色視圖,這個時候重寫紅色視圖的hitTest方法是無效的。
情景2:點擊紅色視圖,綠色視圖響應(也就是事件透傳)
我們可以重寫紅色視圖的hitTest方法,讓其返回空,這時候便沒有了合適的子視圖來響應事件,父視圖即綠色視圖就成為了最合適的響應事件的視圖,代碼示例如下:

當然,我們也可以重寫綠色視圖的hitTest方法,讓其直接返回自身,也能實現(xiàn)同樣效果,不過這樣的話點擊其它子視圖(比如黃色視圖)就也不能響應事件了,因此如何處理需要視情況而定。
-
情景3:點擊紅色視圖,紅色和綠色視圖均做響應
我們知道,事件在不能被處理時,會沿著響應者鏈傳遞給下一個響應者,因此我們可以重寫響應者對象的touches方法來實現(xiàn)讓一個事件多個響應者對象響應的目的。因此我們可以通過重寫紅色視圖的touches方法,先做自己的處理,然后在把事件傳遞給下一個響應者,代碼示例如下:
999.png
需要說明的是,事件傳遞給下一個響應者時,用的是super而不是superview,這并沒有問題,因為super調用了父類的實現(xiàn),而父類默認的實現(xiàn)就是調用下一個響應者的touches方法。如果直接調用superview反而會有問題,因為下一個響應者可能是viewcontroller
手勢識別器
事實上,我們要處理事件除了使用前面提到的方式,還有另一種方式,就是手勢識別器。手勢識別器可以很方便的處理常用的各種觸摸事件,常見的手勢包括單擊、拖動,長按,橫掃或豎掃,縮放,旋轉等,另外我們還可以創(chuàng)建自定義的手勢。
UIGestureRecognize是手勢識別器的父類,所有具體的手勢識別器均繼承于該父類,如果我們自定義手勢,也需要繼承該類。然而,該類并沒有繼承于UIResponder,所以手勢識別器并不參與響應者鏈。那么手勢識別器是如何工作的呢?
手勢識別器工作機制
當觸摸屏幕產(chǎn)生touch事件后,UIApplication會將事件往下分發(fā),如果視圖綁定了手勢識別器,那么touch事件會優(yōu)先傳遞給綁定在視圖上的手勢識別器,然后手勢識別器會對手勢進行識別,如果識別出了手勢,就會調用創(chuàng)建手勢時所綁定的回調方法,并且會取消將touch事件繼續(xù)傳遞給其所綁定的視圖,如果手勢識別器沒有識別出對應的手勢,那么touch事件會繼續(xù)向手勢識別器所綁定的視圖傳遞。
雖然手勢識別器并不是響應者鏈中的一員,但是手勢識別器像一個觀察者,會在一旁觀察touch事件,并延遲事件向所綁定的視圖傳遞,這短暫的延遲使手勢識別器有機會優(yōu)先去識別手勢處理touch事件。
標準控件的事件處理
對于UIKit提供的的標準控件,可以很方便地通過Target-Action的方式增加事件處理邏輯(例如UIButton的addTarget方法),那么Target-Action,手勢識別器,以及touches方法的優(yōu)先順序是怎樣的呢?
情景1
我們以UIbutton為例,首先繼承UIbutton并重寫touches方法,然后創(chuàng)建button對象并綁定單擊手勢,然后再通過addtarget的方式添加點擊事件。三者同時存在時,手勢識別器優(yōu)先響應,其他方式不再響應,手勢識別器不存在時,touches方法優(yōu)先響應,僅當UIbutton沒有綁定手勢識別器,也沒有被重寫touches方法時,target-action方式才會響應。這里我們也可以推測target-action方式應該就是重寫了button的touches方法情景2
仍以UIbutton為例,我們創(chuàng)建button對象,并在button的父視圖上綁定手勢(或者重寫父視圖的touches方法),結果是button的target-action方式優(yōu)先進行了響應,父視圖并沒有響應。這也很顯然,從hittest的遞歸邏輯看,當發(fā)現(xiàn)了合適的子視圖(button)時就直接由子視圖第一響應,父視圖將不是最合適的響應者,當然它處于響應者鏈的上一層。

