一、什麼是響應鏈?
大多數事件的分發都是依賴響應鏈的。
響應鏈
是由一系列連結在一起的響應者
(UIResponse子類:UIApplication
、UIViewController
、UIView
)組成的。一般情況下,一條響應鏈開始於第一響應者,結束於application物件。如果一個響應者不能處理事件,則會將事件沿著響應鏈傳到下一響應者。
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
複製程式碼
二、事件的傳遞與響應
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的邏輯
2.2、事件的響應:一旦事件的第一響應者確定了,這個事件所處的響應鏈就確定了
案例一:下圖是官網對於響應鏈的示例展示
- 圖中虛線箭頭是指若該
UIView
是作為UIViewController
根檢視存在的,則其nextResponder
為UIViewController
物件; - 若是直接add在
UIWindow
上的,則其nextResponder
為UIWindow物件。
// 若觸控發生在UITextField上,則事件的傳遞順序是:
UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation
複製程式碼
案例二:參考下圖
- 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];
}
複製程式碼
事件的生命週期
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:
方法
- → 1、先呼叫父控制元件的
-
- → 2、呼叫最上面子控制元件的
point:inside:
方法
- → 2、呼叫最上面子控制元件的
-
- → 3、如果最上面子控制元件的
point:inside:
方法返回false,則對應的hitTest返回nil
- → 3、如果最上面子控制元件的
-
- → 4、如果最上面子控制元件的
point:inside:
方法返回true,則呼叫對應的hitTest方法重複上面的操作返回子控制元件的最合適子控制元件
- → 4、如果最上面子控制元件的
疑問?
UIGestureRecongnizer
、UIContorl
都可以處理觸控事件-
UIGestureRecognizer
:使用addGestureRecognizer
方法處理事件
-
UIControl
:使用addTarget
方法處理事件
-
UIResponder
:使用touches
等一系列方法處理事件
UIButton繼承自UIControl
,UIControl繼承自UIView
,如果給UIButton
新增了手勢,並實現了自己的處理事件的>>方法,當點選UIButton
的時候發現touches
方法走了,手勢方法(addGestureRecognizer
)也走了,自己的方法(addTarget
)沒有走。由此可以得出一個結論:
UIGestureRecognizer的優先順序
> >UIContol的優先順序
,當一個UIButton即實現了自己的方法(addTarget
),又新增了手勢方法(addGestureRecognizer
)的話,自己的方法(addTarget
)會被遮蔽掉,不管是否新增了手勢,touches
方法都會處理。