理解響應者和響應鏈如何處理事件

godiscoder發表於2018-11-05

事件在APP中如何傳遞

先看一下Apple文件是如何解釋事件的傳遞:

  APP通過UIResponder的例項來處理事件。UIResponder 是UIView/UIViewController/ UIApplication的父類。Responder接受到事件之後只能選擇處理事件
或者將事件傳遞給另一個responder物件,當你的APP接收到一個事件時,UIKit會自動最合適的responder,這就是第一響應者(first responder)。未處理的事件
是會在一個有效的響應鏈裡傳遞的,這條響應鏈是由你的APP的responder動態配置的。如果你當前接受事件的view沒有處理事件,UIKit會將事件傳遞給它的父檢視,
如果後面的view都沒有處理事件的話,它會一次往後傳遞直到UIApplication。它的路徑大概是View->root View->Viewcontroller->root Viewcontroller-
>UIWindow->UIApplication。
注意:如果你UIResponder的例項已經不再響應鏈之中的話,事件會再從UIApplication傳遞到app delegate 
複製程式碼

通過上文的文件我們可以看到,我們的APP之所以能夠處理事件都是依靠responder物件。如果當前接受 到事件的view沒有處理事件,該事件會一直傳遞到UIApplication。

決定first responder

現在我們知道UIKit會自動指定first responder,接下來我們看一下都支援哪些型別的事件:

Event Type                 First responder
Touch events               發生touch事件的那個view
Press events               聚焦的那個物件
Shake-motion events        由你或者UIKit指定
Remote-control events      由你或者UIKit指定
Editing menu messages      由你或者UIKit指定

注意:
1.Motion events 並不遵循responder chain
2.Control的Action messages並不是events,但是它們也利用responder chain
複製程式碼

通過文件我們知道UIKit會自動查詢first responder,那麼它是如何實現這個查詢過程呢?通過文件 我們可以瞭解到它底層是通過view-based hit-testing來確定哪裡發生了event,具體是通過hitTest:withEvent:方法來查詢view,將會成為first responder。

注意:下面幾種情況hitTest:withEvent: 方法會直接忽略該view和其所有的subView
1、view的bounds不包含touch的位置
2、如果view的clipsToBounds為false,它的subView的bounds超出了view的bounds,即使該subView的bounds包含touch的位置
複製程式碼

現在我們知道了UIKit通過hitTest:withEvent:方法來確定first responder,那麼它是如 何確定touch的位置的呢?這個問題我們還是要看一下文件說明:

  當觸控發生時,UIKit會建立一個UITouch物件,並將其與檢視相關聯。隨著觸控位置或其他引數的
變化,UIKit會用新資訊更新同一個UITouch物件。唯一不變的屬性是檢視。(即使觸控位置移動到原始檢視之外,觸控的檢視屬性中的值也不會改變。)當觸控結束時,UIKit釋放UITouch物件。
複製程式碼

看完這段話就知道了UIKit是根據UITouch物件來判斷的touch的位置。 OK,現在我們看一下event分發的流程參考:

1.觸碰螢幕產生事件UIEvent並存入UIApplication中事件佇列中,並在整個檢視結構中自下而上進行分發,如下圖
2.UIWindow 接收到事件開始進行最優響應檢視查詢過程(逆序遍歷子檢視)
3.當到UiviewController這一層時同樣對其檢視開始最優響應檢視查詢,該查詢會呼叫上述提到的兩個方法,採用逆序查詢也是為了優化查詢速度,畢竟後新增的檢視易於命中
複製程式碼

修改responder chain

你可以通過重寫你responder object的nextResponder來修改responder chain。很多UIKit的類已經重寫了該屬性並返回具體的物件。具體詳情請看文件

實際應用

在日常開發中我們經常會碰到button超出父檢視的需求,在這種情況下,超出部分是預設不相應的。這時候我們要重寫父檢視的func point(inside point: CGPoint, with event: UIEvent?) -> Bool方法來讓父檢視響應超出的部分。

效果圖:

理解響應者和響應鏈如何處理事件

程式碼示例:

class RootView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        //超出範圍
        let radius: CGFloat = 10.0
        print(self.subviews.count)
        var largeBounds = bounds
        largeBounds = CGRect(x: bounds.origin.x - radius, y: bounds.origin.y - radius, width: bounds.size.width + radius, height: bounds.size.height + radius)
        return largeBounds.contains(point)
    }
}
這樣超出部分就可以點選響應事件了.
複製程式碼

總結

  • event分發與傳遞:自上而下(UIApplication-window-.....)
  • event響應:自下而上(view-superView-.........)

本文gihut地址

Link

相關文章