史上最詳細的iOS之事件的傳遞和響應機制

發表於2016-03-02

前言:

按照時間順序,事件的生命週期是這樣的:
事件的產生和傳遞(事件如何從父控制元件傳遞到子控制元件並尋找到最合適的view、尋找最合適的view的底層實現、攔截事件的處理)->找到最合適的view後事件的處理(touches方法的重寫,也就是事件的響應)

其中重點和難點是:
1.如何尋找最合適的view
2.尋找最合適的view的底層實現(hitTest:withEvent:底層實現)

(一)iOS中的事件

iOS中的事件可以分為3大型別:

  • 觸控事件
  • 加速計事件
  • 遠端控制事件
    這裡我們只討論iOS中的觸控事件。

1.1.響應者物件(UIResponder)

學習觸控事件首先要了解一個比較重要的概念-響應者物件(UIResponder)。

  • UIApplication
  • UIViewController
  • UIView

那麼為什麼繼承自UIResponder的類就能夠接收並處理事件呢?

  • 因為UIResponder中提供了以下4個物件方法來處理觸控事件。

(二)事件的處理

下面以UIView為例來說明觸控事件的處理。

需要注意的是:以上四個方法是由系統自動呼叫的,所以可以通過重寫該方法來處理一些事件。

  • 如果兩根手指同時觸控一個view,那麼view只會呼叫一次touchesBegan:withEvent:方法,touches引數中裝著2個UITouch物件
  • 如果這兩根手指一前一後分開觸控同一個view,那麼view會分別呼叫2次touchesBegan:withEvent:方法,並且每次呼叫時的touches引數中只包含一個UITouch物件
  • 重 寫以上四個方法,如果是處理UIView的觸控事件。必須要自定義UIView子類繼承自UIView。因為蘋果不開源,沒有把UIView的.m檔案提 供給我們。我們只能通過子類繼承父類,重寫子類方法的方式處理UIView的觸控事件(注意:我說的是UIView觸控事件而不是說的 UIViewController的觸控事件)。
  • 如果是處理UIViewController的觸控事件,那麼在控制器的.m檔案中直接重寫那四個方法即可!

/**自定義UIView的.h檔案**/

/**自定義UIView的.m檔案*/

/**控制器的.m檔案*/

注 意:有人認為,我要是處理控制器的自帶的view的事件就不需要自定義UIView子類繼承於UIView,因為可以在viewController.m 檔案中重寫touchBegan:withEvent:方法,但是,我們此處討論的是處理UIView的觸控事件,而不是處理 UIViewController的觸控事件。你如果是在viewController.m檔案中重寫touchBegan:withEvent:方法,相當於處理的是viewController的觸控事件,因為viewController也是繼承自UIResponder,所以會給人一種錯覺。
所以,還是那句話,想處理UIView的觸控事件,必須自定義UIView子類繼承自UIView。

2.1.UIView的拖拽

那麼,如何實現UIView的拖拽呢?也就是讓UIView隨著手指的移動而移動。
– 重寫touchsMoved:withEvent:方法
此時需要用到引數touches,下面是UITouch的屬性和方法:

2.1.1.UITouch物件

  • 當使用者用一根手指觸控螢幕時,會建立一個與手指相關的UITouch物件
  • 一根手指對應一個UITouch物件
  • 如果兩根手指同時觸控一個view,那麼view只會呼叫一次touchesBegan:withEvent:方法,touches引數中裝著2個UITouch物件
  • 如果這兩根手指一前一後分開觸控同一個view,那麼view會分別呼叫2次touchesBegan:withEvent:方法,並且每次呼叫時的touches引數中只包含一個UITouch物件

2.1.1.1.UITouch的作用

  • 儲存著跟手指相關的資訊,比如觸控的位置、時間、階段
  • 當手指移動時,系統會更新同一個UITouch物件,使之能夠一直儲存該手指在的觸控位置
  • 當手指離開螢幕時,系統會銷燬相應的UITouch物件
    提 示:iPhone開發中,要避免使用雙擊事件!

2.1.1.2.UITouch的屬性

2.1.1.3.UITouch的方法

程式碼實現:


(三)iOS中的事件的產生和傳遞

3.1.事件的產生

  • 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的事件佇列中為什麼是佇列而不是棧?因為佇列的特定是先進先出,先產生的事件先處理才符合常理,所以把事件新增到佇列。
  • UIApplication會從事件佇列中取出最前面的事件,並將事件分發下去以便處理,通常,先傳送事件給應用程式的主視窗(keyWindow)。
  • 主視窗會在檢視層次結構中找到一個最合適的檢視來處理觸控事件,這也是整個事件處理過程的第一步。
    找到合適的檢視控制元件後,就會呼叫檢視控制元件的touches方法來作具體的事件處理。

    3.2.事件的傳遞

  • 觸控事件的傳遞是從父控制元件傳遞到子控制元件
  • 也就是UIApplication->window->尋找處理事件最合適的view

注 意: 如果父控制元件不能接受觸控事件,那麼子控制元件就不可能接收到觸控事件

應用如何找到最合適的控制元件來處理事件?
  • 1.首先判斷主視窗(keyWindow)自己是否能接受觸控事件
  • 2.判斷觸控點是否在自己身上
  • 3.子控制元件陣列中從後往前遍歷子控制元件,重複前面的兩個步驟(所謂從後往前遍歷子控制元件,就是首先查詢子控制元件陣列中最後一個元素,然後執行1、2步驟)
  • 4.view,比如叫做fitView,那麼會把這個事件交給這個fitView,再遍歷這個fitView的子控制元件,直至沒有更合適的view為止。
  • 5.如果沒有符合條件的子控制元件,那麼就認為自己最合適處理這個事件,也就是自己是最合適的view。

UIView不能接收觸控事件的三種情況:

  • 不允許互動:userInteractionEnabled = NO
  • 隱藏:如果把父控制元件隱藏,那麼子控制元件也會隱藏,隱藏的控制元件不能接受事件
  • 透明度:如果設定一個控制元件的透明度

注 意:預設UIImageView不能接受觸控事件,因為不允許互動,即userInteractionEnabled = NO,所以如果希望UIImageView可以互動,需要userInteractionEnabled = YES。

總結一下

1.點選一個UIView或產生一個觸控事件A,這個觸控事件A會被新增到由UIApplication管理的事件佇列中(即,首先接收到事件的是UIApplication)。
2.UIApplication會從事件對列中取出最前面的事件(此處假設為觸控事件A),把事件A傳遞給應用程式的主視窗(keyWindow)。
3.視窗會在檢視層次結構中找到一個最合適的檢視來處理觸控事件。(至此,第一步已完成)
[圖片上傳中。。。(1)]
如果想讓某個view不能接收事件(或者說,事件傳遞到某個view那裡就斷了),那麼可以通過剛才提到的三種方式。比如,設定其userInteractionEnabled = NO;那麼傳遞下來的事件就會由該view的父控制元件處理。
例如,不想讓藍色的view接收事件,那麼可以設定藍色的view的userInteractionEnabled = NO;那麼點選黃色的view或者藍色的view所產生的事件,橙色的view就會成為最合適的view。事件都會由橙色的veiw處理。
所以,不管檢視能不能處理事件,只要點選了檢視就都會產生事件,關鍵看該事件是由誰來處理!也就是說,如果檢視不能處理事件,點選檢視,還是會產生一個觸控事件,只是該事件不會由被點選的檢視處理而已!
注意:如果設定父控制元件的透明度或者hidden,會直接影響到子控制元件的透明度和hidden。如果父控制元件的透明度為0或者hidden = YES,那麼子控制元件也是不可見的!

3.3.(重難點)如何尋找最合適的view

應用如何找到最合適的控制元件來處理事件?
1.首先判斷主視窗(keyWindow)自己是否能接受觸控事件
2.觸控點是否在自己身上
3.從後往前遍歷子控制元件,重複前面的兩個步驟(首先查詢陣列中最後一個元素)
4.如果沒有符合條件的子控制元件,那麼就認為自己最合適處理

詳述:1.主視窗接收到應用程式傳遞過來的事件後,首先判斷自己能否接手觸控事件。如果能,那麼在判斷觸控點在不在視窗自己身上
2.如果觸控點也在視窗身上,那麼視窗會從後往前遍歷自己的子控制元件(遍歷自己的子控制元件只是為了尋找出來最合適的view)
3.遍歷到每一個子控制元件後,又會重複上面的兩個步驟(傳遞事件給子控制元件,1.判斷子控制元件能否接受事件,2.點在不在子控制元件上)
4.如此迴圈遍歷子控制元件,直到找到最合適的view,如果沒有更合適的子控制元件,那麼自己就成為最合適的view。
找到最合適的view後,就會呼叫該view的touches方法處理具體的事件。所以,只有找到最合適的view,把事件傳遞給最合適的view後,才會呼叫touches方法進行接下來的事件處理。找不到最合適的view,就不會呼叫touches方法進行事件處理。
注意:之所以會採取從後往前遍歷子控制元件的方式尋找最合適的view只是為了做一些迴圈優化。因為相比較之下,後新增的view在上面,降低迴圈次數。

3.3.1.尋找最合適的view底層剖析

兩個重要的方法:
hitTest:withEvent:方法
pointInside方法

3.3.1.1.hitTest:withEvent:方法

什麼時候呼叫?

  • 只要事件一傳遞給一個控制元件,這個控制元件就會呼叫他自己的hitTest:withEvent:方法

作用

  • 尋找並返回最合適的view(能夠響應事件的那個最合適的view)

注 意:不管這個控制元件能不能處理事件,也不管觸控點在不在這個控制元件上,事件都會先傳遞給這個控制元件,隨後再呼叫hitTest:withEvent:方法

攔截事件的處理

  • 正因為hitTest:withEvent:方法可以返回最合適的view,所以可以通過重寫hitTest:withEvent:方法,返回指定的view作為最合適的view。
  • 不管點選哪裡,最合適的view都是hitTest:withEvent:方法中返回的那個view。
  • 通過重寫hitTest:withEvent:,就可以攔截事件的傳遞過程,想讓誰處理事件誰就處理事件。

事件傳遞給誰,就會呼叫誰的hitTest:withEvent:方法。
注 意:如果hitTest:withEvent:方法中返回nil,那麼呼叫該方法的控制元件本身和其子控制元件都不是最合適的view,也就是在自己身上沒有找到更合適的view。那麼最合適的view就是該控制元件的父控制元件。
所以事件的傳遞順序是這樣的:
產生觸控事件->UIApplication事件佇列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控制元件 hitTest:withEvent:]->返回最合適的view

事件傳遞給視窗或控制元件的後,就呼叫hitTest:withEvent:方法尋找更合適的view。所以是,先傳遞事件,再根據事件在自己身上找更合適的view。
不管子控制元件是不是最合適的view,系統預設都要先把事件傳遞給子控制元件,經過子控制元件呼叫自己的hitTest:withEvent:方法驗證後才知道有沒有更合適的view。即便父控制元件是最合適的view了,子控制元件的hitTest:withEvent:方法還是會呼叫,不然怎麼知道有沒有更合適的!即,如果確定最終父控制元件是最合適的view,那麼該父控制元件的子控制元件的hitTest:withEvent:方法也是會被呼叫的。
技巧:想讓誰成為最合適的view就重寫誰自己的父控制元件的hitTest:withEvent:方法返回指定的子控制元件,或者重寫自己的hitTest:withEvent:方法 return self。但是,建議在父控制元件的hitTest:withEvent:中返回子控制元件作為最合適的view!

原因在於在自己的hitTest:withEvent:方法中返回自己有時候會出現問題,因為會存在這麼一種情況,當遍歷子控制元件時,如果觸控點不在子控制元件A自己身上而是在子控制元件B身上,還要要求返回子控制元件A作為最合適的view,採用返回自己的方法可能會導致還沒有來得及遍歷A自己,就有可能已經遍歷了點真正所在的view,也就是B。這就導致了返回的不是自己而是點真正所在的view。所以還是建議在父控制元件的hitTest:withEvent:中返回子控制元件作為最合適的view!
例如:whiteView有redView和greenView兩個子控制元件。redView先新增,greenView後新增。如果要求無論點選那裡都要讓redView作為最合適的view(把事件交給redView來處理)那麼只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];這種情況下在redView的hitTest:withEvent:方法中return self;是不好使的!

特殊情況:
誰都不能處理事件,視窗也不能處理。

  • 重寫window的hitTest:withEvent:方法return nil

只能有視窗處理事件。

  • 控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self

return nil的含義:
hitTest:withEvent:中return nil的意思是呼叫當前hitTest:withEvent:方法的view不是合適的view,子控制元件也不是合適的view。如果同級的兄弟控制元件也沒有合適的view,那麼最合適的view就是父控制元件。

尋找最合適的view底層剖析之hitTest:withEvent:方法底層做法
/** hitTest:withEvent:方法底層實現**/

hit:withEvent:方法底層會呼叫pointInside:withEvent:方法判斷點在不在方法呼叫者的座標系上。

3.3.1.2.pointInside:withEvent:方法

pointInside:withEvent:方法判斷點在不在當前view上(方法呼叫者的座標系上)如果返回YES,代表點在方法呼叫者的座標系上;返回NO代表點不在方法呼叫者的座標系上,那麼方法呼叫者也就不能處理事件。


(四)事件的響應

4.1.觸控事件處理的整體過程

1>使用者點選螢幕後產生的一個觸控事件,經過一系列的傳遞過程後,會找到最合適的檢視控制元件來處理這個事件2>找到最合適的檢視控制元件後,就會呼叫控制元件的touches方法來作具體的事件處理touchesBegan…touchesMoved…touchedEnded…3>這些touches方法的預設做法是將事件順著響應者鏈條向上傳遞(也就是touch方法預設不處理事件,只傳遞事件),將事件交給上一個響應者進行處理

4.2.響應者鏈條示意圖

響應者鏈條:在iOS程式中無論是最後面的UIWindow還是最前面的某個按鈕,它們的擺放是有前後關係的,一個控制元件可以放到另一個控制元件上面或下面,那麼使用者點選某個控制元件時是觸發上面的控制元件還是下面的控制元件呢,這種先後關係構成一個鏈條就叫“響應者鏈”。也可以說,響應者鏈是由多個響應者物件連線起來的鏈條。在iOS中響應者鏈的關係可以用下圖表示:

響應者物件:能處理事件的物件,也就是繼承自UIResponder的物件
作用:能很清楚的看見每個響應者之間的聯絡,並且可以讓一個事件多個物件處理。

如何判斷上一個響應者

  • 1> 如果當前這個view是控制器的view,那麼控制器就是上一個響應者
  • 2> 如果當前這個view不是控制器的view,那麼父控制元件就是上一個響應者

響應者鏈的事件傳遞過程:

  • 1>如果當前view是控制器的view,那麼控制器就是上一個響應者,事件就傳遞給控制器;如果當前view不是控制器的view,那麼父檢視就是當前view的上一個響應者,事件就傳遞給它的父檢視
  • 2>在檢視層次結構的最頂級檢視,如果也不能處理收到的事件或訊息,則其將事件或訊息傳遞給window物件進行處理
  • 3>如果window物件也不處理,則其將事件或訊息傳遞給UIApplication物件
  • 4>如果UIApplication也不能處理該事件或訊息,則將其丟棄

事件處理的整個流程總結:
1.觸控螢幕產生觸控事件後,觸控事件會被新增到由UIApplication管理的事件佇列中(即,首先接收到事件的是UIApplication)。
2.UIApplication會從事件佇列中取出最前面的事件,把事件傳遞給應用程式的主視窗(keyWindow)。
3.主視窗會在檢視層次結構中找到一個最合適的檢視來處理觸控事件。(至此,第一步已完成)
4.最合適的view會呼叫自己的touches方法處理事件
5.touches預設做法是把事件順著響應者鏈條向上拋。
touches的預設做法:

事件的傳遞與響應:
1、當一個事件發生後,事件會從父控制元件傳給子控制元件,也就是說由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的傳遞,也就是尋找最合適的view的過程。

2、接下來是事件的響應。首先看initial view能否處理這個事件,如果不能則會將事件傳遞給其上級檢視(inital view的superView);如果上級檢視仍然無法處理則會繼續往上傳遞;一直傳遞到檢視控制器view controller,首先判斷檢視控制器的根檢視view是否能處理此事件;如果不能則接著判斷該檢視控制器能否處理此事件,如果還是不能則繼續向上傳 遞;(對於第二個圖檢視控制器本身還在另一個檢視控制器中,則繼續交給父檢視控制器的根檢視,如果根檢視不能處理則交給父檢視控制器處理);一直到 window,如果window還是不能處理此事件則繼續交給application處理,如果最後application還是不能處理此事件則將其丟棄

3、在事件的響應中,如果某個控制元件實現了touches…方法,則這個事件將由該控制元件來接受,如果呼叫了[supertouches….];就會將事件順著響應者鏈條往上傳遞,傳遞給上一個響應者;接著就會呼叫上一個響應者的touches….方法

如何做到一個事件多個物件處理:
因為系統預設做法是把事件上拋給父控制元件,所以可以通過重寫自己的touches方法和父控制元件的touches方法來達到一個事件多個物件處理的目的。

事件的傳遞和響應的區別:
事件的傳遞是從上到下(父控制元件到子控制元件),事件的響應是從下到上(順著響應者鏈條向上傳遞:子控制元件到父控制元件。

如果您是iOS開發者,或者對本篇文章感興趣,請關注本人,後續會更新更多相關文章!敬請期待!

相關文章