iOS事件傳遞
本篇主要講解iOS事件傳遞的整個過程,大部分內容翻譯自Apple Developer Guide
當一個用戶事件產生的時候,UIKit 會創建一個事件對象來描述這個用戶事件。然后它會將該事件對象放進UIApplication對象所維護的事件隊列中。對于觸摸事件而言,產生的事件對象便是一個包含UIEvent的集合(NSSet)對象。
一個事件會朝著特定的路徑進行傳遞直到遇到一個可以處理它的對象為止。首先,UIApplication對象會從事件隊列的棧頂拿到事件,并且通過分發下去的方式處理該事件。一般情況下,UIApplication發送該事件到app的主窗口對象(key window object),主窗口對象會根據事件類型(Touch event、Motion and remote control events)選擇一個相應的對象,將事件傳遞給它。
- Touch events. 窗口對象首先會嘗試將事件傳遞給事件發生所在的view對象。這個view對象我們稱之為 hit-test view。尋找這個hit-test view的過程叫做hit-testing(定位Touch事件發生在哪個view上)。
- Motion and remote control events. 就這些事件而言,窗口對象會將 shaking-motion 或者 remote control event發送給響應者鏈條的第一個響應者(the first responder)去處理。
這些事件傳遞鏈條的最終目標是發現一個能夠響應和處理該事件的對象。因此,UIKit首先將事件傳遞給一個最適合處理它的對象。就Touch events來說,該對象就是hit-test view,對于其它事件而言,該對象是第一個響應者。下面的章節介紹hit-test view和第一響應者是怎么確定的。
通過hit-test找到觸摸的View
當用戶觸摸屏幕的時候,iOS會使用 hit-testing去找到用戶觸摸的view對象。hit-testing機制會在所有相關view中檢查觸摸是否發生在哪一個view的范圍內中,如果找到了,那么通過遞歸的方式繼續檢查這個view的子view們。當遞歸結束的時候,處于view層級結構中最下面的那個包含觸摸點的view就是 hit-test view。在iOS確定了 hit-test view之后,會將觸摸事件傳遞給它尋求處理。
圖2-1
舉個例子,假設用戶觸摸了圖2-1中的 view E。iOS通過下面的步驟來檢查subviews,最終確定hit-test view:
- 觸摸點在 view A里面,所以檢查它的子控件 B 和 C。
- 觸摸點不在 view B里面,但是在 view C里面,所以檢查它的子控件 D 和 E。
- 觸摸點不在 view D里面,但是在 view E里面。view E是包含該觸摸點的最底層 view,所以它就是 hit-test view.
傳遞一個CGPoint 和 UIEvent對象給UIView 的 hitTest:withEvent: 方法,它會返回 hit-test view對象。hitTest:withEvent: 方法開始的時候會調用自身的 pointInside:withEvent: 方法。如果傳遞給 hitTest:withEvent: 的點在view對象的范圍內,pointInside:withEvent: 會返回YES。如果返回YES,那么父view的 hitTest:withEvent: 會繼續通過遞歸的方式調用子view的 hitTest:withEvent 方法。
如果傳遞給hitTest:withEvent:的點不在根view的范圍內,那么調用 pointInside:withEvent: 會返回NO,這個點就被忽略,然后 hitTest:withEvent: 會返回nil。如果一個子view返回NO,那么從該子view到它的所有子view的遞歸分支會被忽略,因為如果一個觸摸點不在子view的范圍內,那么同樣的,這個觸摸點不會在該子view的所有子view范圍內。這意味著,當觸摸的點所在的范圍是在父view之外,那么子view是無法接收到觸摸事件的,因為事件接收的充要條件是觸摸點必須同時處在父view和子view的范圍內。這種情況發生在子view的大小超過了父view的大小并且子view的clipsToBounds屬性值為NO,并且觸摸點超過父view的范圍,但是位于子view的范圍內的時候。
注意:觸摸事件對象的整個生命周期都會和它的 hit-test view關聯,即使觸摸最后超出了這個hit-test view的范圍
hit-test view作為第一個可能處理touch event的對象,如果它不能處理這個事件,那么事件會沿著響應者鏈條往上傳遞給上一個響應者,直到系統找到一個可以處理這個事件的對象為止。
響應者鏈條是由響應者對象構成的
許多類型的事件都依賴響應者鏈條進行事件傳遞。響應者鏈條是一連串連接在一起的響應者對象。它從第一響應者開始到UIApplication對象結束。如果第一響應者不能處理事件,系統會將事件傳遞給響應者鏈條的下一個響應者。
響應者對象是一個能夠響應和處理事件的對象。它們有一個共同點就是都是繼承自UIResponder,UIResponder的程序接口不僅定義了事件處理方式,還有響應者的一些共同行為。UIApplication,UIViewController和UIView都是響應者,這意味著UIView與其所有子類和大多數主要控制器對象都是響應者,注意 Core Animation layers 不是響應者。
第一響應者被設計作為第一個接收事件的對象,一般而言,第一響應者是一個view對象。一個對象通過下面的兩個步驟成為第一響應者:
- 覆蓋 canBecomeFirstResponder方法并且返回YES。
- 接收到一個 becomeFirstResponder 消息,如果有需要,一個對象可以給自己發送這個消息。
注意:在讓一個對象成為第一響應者之前,確保你的app已經渲染好它的視圖,舉個例子,我們一般在viewDidAppear:方法中調用becomeFirstResponder方法,如果我們嘗試在viewWillAppear: 方法中調用,由于我們的視圖還沒有渲染到屏幕上,所以 becomeFirstResponder 方法會返回NO。
事件并不是唯一一種依賴響應者鏈條進行傳遞的對象。響應者鏈條被用在下面的情況中:
- Touch events. 如果 hit-test view不能處理觸摸事件,那么事件會順著響應者鏈條從 hit-test view 開始傳遞上去。
- Motion events. 為了讓UIKit處理 shake-motion 事件,第一響應者必須覆蓋UIRespnder的 motionBegan:withEvent: 或者 motionEnded:withEvent: 方法。
- Remote control events. 為了處理 remote control 事件,第一響應者必須實現UIResponder的 remoteControlReceivedWithEvent: 方法
- Action messages. 當用戶對一個UIControl對象進行操作的時候,比如一個button或者switch,如果沒有為該對象指定target和action method,那么一個Action message方法會從第一響應者開始順著響應者鏈傳遞出去,這個第一響應者可以是這個UIControl對象本身。
- Editing-menu messages.當用戶使用了編輯菜單的一個指令選項的時候,iOS使用響應者鏈條去找一個實現了對應必要方法(例如cut:,copy: 和 paste: )的對象。
- Text editing. 當用戶觸摸一個text field 或者是 text view的時候,這個view自動成為第一響應者。默認的,這個時候虛擬鍵盤會出現并且text field或text view會變成編輯狀態。如果需要,你可以使用一個自定義的輸入視圖代替鍵盤。同樣的,你可以為任何響應者對象添加一個自定義的輸入視圖(通過重定義inputView屬性為readwrite并且賦值)。
在觸摸text field和text view的時候,UIKit自動設置它們為第一響應者;如果想讓其它UIResponder成為第一響應者,我們必須在代碼中設置調用該對象的 becomeFirstRespnder 方法。
響應者鏈條的事件傳遞路徑是遵循特定的規則形成的
如果最初的對象不是hit-test view或者第一響應者沒有處理事件,UIKit會將事件傳遞給鏈條中的下一個響應者。每一個響應者都可以決定是去處理這個事件,還是通過調用 nextResponder 方法將事件傳遞給下一個響應者。這個過程會持續進行下去,直到找到一個能處理事件的響應者或者到UIApplication對象結束。
在iOS檢測到一個事件并且將它傳遞給初始對象(一般是一個view)的時候,響應者鏈條開始有序地運作。初始view是第一個可能處理響應事件的對象。圖2-2展示在兩種界面視圖結構下的兩條事件傳遞路徑。一個app的事件傳遞路徑依賴于它的視圖結構,不同的視圖結構可能有不同的事件傳遞路徑,但是所有事件傳遞路徑都是有跡可循的。
圖2-2
左邊的app,它的事件傳遞過程如下:
- initial view 嘗試去處理事件或者消息。如果它不能處理事件,它將事件傳遞給它的父控件,因為initial view 不是它所在控制器的直接視圖,所以是將事件往上拋給父控件而不是控制器對象。
- 它的父控件嘗試去處理事件。如果父控件不能處理事件,繼續將事件傳遞給它的父控件,因為它也不是控制器的直接視圖。
- 控制器的直接視圖(controller.view)是所在控制器視圖中層級最高的,如果這個視圖還是無法處理事件,那么它會將事件拋給控制器。
- 控制器嘗試去處理事件,如果不能處理事件,它會將事件拋給window對象。
- 如果window對象無法處理事件,window將事件拋給UIApplication對象。
- 如果UIApplication對象無法處理事件,那么就銷毀這個事件。
右邊的app,它的事件傳遞過程明顯和第左邊的不同,但是所有事件傳遞都有如下規則:
- 一個view順著控制器視圖的層級結構傳遞事件,直到到達控制器的直接視圖,也就是最頂層的視圖。
- 頂層視圖將事件傳遞給它所在的控制器。
- 控制器傳遞事件給頂層視圖的父控件。
步驟 1-3 重復直到到達根控制器。 - 根控制器將事件拋給window對象。
- window對象將事件拋給UIApplication對象。
注意:當你在處理remote control events,action messages, shake-motion events 或者 editing-menu mesages的時候,如果想通過響應者鏈條傳遞事件,不要直接調用nextResponder,而是通過調用父類實現好事件處理方法,讓UIKit去做事件傳遞。
來自:http://www.jianshu.com/p/4af8ea6b09a3