UIResponder事件響應鏈學習筆記

在路上重名了啊發表於2018-10-27

一、什麼是響應鏈?

大多數事件的分發都是依賴響應鏈的。響應鏈是由一系列連結在一起的響應者(UIResponse子類:UIApplicationUIViewControllerUIView)組成的。一般情況下,一條響應鏈開始於第一響應者,結束於application物件。如果一個響應者不能處理事件,則會將事件沿著響應鏈傳到下一響應者。

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
複製程式碼

二、事件的傳遞與響應

UIResponder事件響應鏈學習筆記

2.1、事件的傳遞:尋找事件的第一響應者(Hit-Testing)

事件被蘋果分為3種大型別: 觸控事件加速計事件以及遠端遙控事件

當一個事件發生後,事件會從父控制元件傳給子控制元件,也就是說由

硬體 -> 系統 -> UIApplication -> UIWindow -> SuperView -> SubView

以上就是事件的傳遞,也就是尋找第一響應者的過程。 符合第一響應者的條件包括:

  • touch事件的位置在響應者區域內 pointInside:withEvent: == YES
  • 響應者 self.hidden != NO
  • 響應者 self.alpha > 0.01
  • 響應者 self.userInteractionEnabled = YES
  • 遍歷 subview 時,是從上往下順序遍歷的,即 view.subviews 的 lastObject 到 firstObject 的順序,找到合適的響應者view,即停止遍歷.

第一響應者對於接收到的事件有3種操作:

  • 不攔截,預設操作。事件會自動沿著預設的響應鏈往下傳遞
  • 攔截,不再往下分發事件。重寫 touchesBegan:withEvent: 進行事件處理,不呼叫父類的 touchesBegan:withEvent:
  • 攔截,繼續往下分發事件。重寫 touchesBegan:withEvent: 進行事件處理,同時呼叫父類的 touchesBegan:withEvent: 將事件往下傳遞

下圖展示了Hit-Testing的邏輯

UIResponder事件響應鏈學習筆記

2.2、事件的響應:一旦事件的第一響應者確定了,這個事件所處的響應鏈就確定了

案例一:下圖是官網對於響應鏈的示例展示

UIResponder事件響應鏈學習筆記

  • 圖中虛線箭頭是指若該UIView是作為UIViewController根檢視存在的,則其nextResponderUIViewController物件;
  • 若是直接add在UIWindow上的,則其nextResponder為UIWindow物件。
// 若觸控發生在UITextField上,則事件的傳遞順序是:
UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation
複製程式碼

案例二:參考下圖

UIResponder事件響應鏈學習筆記

  • 1、 首先由 view 來嘗試處理事件,如果他處理不了,事件將被傳遞到他的父檢視 superview
  • 2、superview 也嘗試來處理事件,如果他處理不了,繼續傳遞他的父檢視 UIViewcontroller.view
  • 3、UIViewController.view 嘗試來處理該事件,如果處理不了,將把該事件傳遞給 UIViewController
  • 4、UIViewController 嘗試處理該事件,如果處理不了,將把該事件傳遞給主視窗 Window
  • 5、主視窗 Window 嘗試來處理該事件,如果處理不了,將傳遞給應用單例 Application
  • 6、如果 Application 也處理不了,則該事件將會被丟棄

事件的傳遞和響應的區別?

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

如何判斷上一個響應者?

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

響應者鏈條的事件傳遞過程?

如果view 的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父檢視 在檢視層次結構的最頂級檢視,如果也不能處理收到的事件或訊息,則其將事件或訊息傳遞給 window 物件進行處理 如果 window 物件也不處理,則其將事件或訊息傳遞給 UIApplication 物件 如果 UIApplication 也不能處理該事件或訊息,則將其丟棄(銷燬)

如何做到一個事件多個物件處理?

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 1.自己先處理事件...
    NSLog(@"do somthing...");
    // 2.再呼叫系統的預設做法,再把事件交給上一個響應者處理
    [super touchesBegan:touches withEvent:event]; 
}
複製程式碼

事件的生命週期

UIResponder事件響應鏈學習筆記

1、系統響應階段

2、APP響應階段

應用場景:

  • 1、 重寫子view的point:inside`` → 擴大Button的點選區域(上下左右各增加20)
  • 2、 重寫父view的point:insde`` →子view超出了父view的bounds響應事件
  • 3、 如果一個Button被一個View蓋住了,在觸控View時,希望該Button能夠響應事件
  • 4、 特殊的UIScrollView
  • 5、 利用響應鏈傳遞自定義UI事件

總結

  • 1、如果父控制元件不能接收觸控事件,則子控制元件也無法接收觸控事件
  • 2、如果想讓控制元件不處理觸控事件,可以設定userInteractionEnabled = NO,結果是包括父控制元件在內的所有子控制元件都不能處理觸控事件(雖然設定透明度和hidden=YES也可以,但是那樣就看不見了注意:如果父控制元件的透明度設定為0或者hidden=YES,那麼子控制元件也是不可見的。)
  • 3、遍歷一個控制元件的子控制元件的順序是從上到下的(最後新增的view最先被遍歷)。
  • 4、指定某一個子控制元件響應事件,只需要在父控制元件的hitTest中返回指定的子控制元件就可以。
  • 5、如果一個控制元件的isUserInteractionEnabled=false,想讓它繼續繼續處理觸控事件,可以在它的父控制元件的hitTest方法中直接返回它。
  • 6、hitTest查詢第一響應者的時候,即便父控制元件是第一響應者,還是要呼叫子控制元件的hitTest方法,否則怎麼知道是不是還有其他最合適的響應者
  • 7、
    • → 1、先呼叫父控制元件的point:inside:方法
    • → 2、呼叫最上面子控制元件的point:inside:方法
    • → 3、如果最上面子控制元件的point:inside:方法返回false,則對應的hitTest返回nil
    • → 4、如果最上面子控制元件的point:inside:方法返回true,則呼叫對應的hitTest方法重複上面的操作返回子控制元件的最合適子控制元件

疑問?

  • UIGestureRecongnizerUIContorl都可以處理觸控事件
    • UIGestureRecognizer:使用addGestureRecognizer方法處理事件
    • UIControl:使用addTarget方法處理事件
    • UIResponder:使用touches等一系列方法處理事件

UIButton繼承自UIControlUIControl繼承自UIView,如果給UIButton新增了手勢,並實現了自己的處理事件的>>方法,當點選UIButton的時候發現touches方法走了,手勢方法(addGestureRecognizer)也走了,自己的方法(addTarget)沒有走。

由此可以得出一個結論:UIGestureRecognizer的優先順序 > >UIContol的優先順序,當一個UIButton即實現了自己的方法(addTarget),又新增了手勢方法(addGestureRecognizer)的話,自己的方法(addTarget)會被遮蔽掉,不管是否新增了手勢,touches方法都會處理。

參考連結:

Hit-Testing in iOS

iOS觸控事件全家桶

iOS事件處理之Hit-Testing

iOS 事件的傳遞響應機制

iOS事件響應鏈中Hit-Test View的應用

UIView的hitTest和pointInside方法

相關文章