一、相關類介紹
1、UIResponder
? ? ? iOS中只有繼承了 UIResponder 的對象才能接收并處理事件。UIResponder 的派生體系如下:
下面是 UIResponder 的接口:
?@interface UIResponder : NSObject
// 響應者相關的屬性和方法
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;?
- (BOOL)becomeFirstResponder; @property(nonatomic, readonly) BOOL canResignFirstResponder;?
- (BOOL)resignFirstResponder;?
@property(nonatomic, readonly) BOOL isFirstResponder;?
?// 觸摸事件?
- (void)touchesBegan:(NSSet*)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet*)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet*)touches withEvent:(nullable UIEvent *)event;?
- (void)touchesCancelled:(NSSet*)touches withEvent:(nullable UIEvent *)event;?
- (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches NS_AVAILABLE_IOS(9_1);
// 關于3Dtouch的按壓事件?
- (void)pressesBegan:(NSSet*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);?
- (void)pressesChanged:(NSSet*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);?
- (void)pressesEnded:(NSSet*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);?
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
// 加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
// 遠程事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
// 其它
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
@end
1.1瘤运、UIResponder 遵守的協(xié)議 UIResponderStandardEditActions,負責處理 復制犁嗅、粘貼 等操作。?
1.2晤碘、UIResponder 可以處理4種事件,分別是:觸摸事件园爷、按壓事件(3Dtouch)宠蚂、加速計事件(搖一搖)童社、遠程事件(耳機操控事件)求厕,當事件發(fā)生時,UIResponder 就會調用對應的方法扰楼。?
1.3呀癣、處理事件時涉及2個對象:UITouch、UIEvent灭抑。(UIPress十艾、UIPressesEvent不作介紹) ?
1.3.1、UITouch?
UITouch是處理手指觸屏交互的底層對象腾节,保存著對應的時間、階段等相關信息荤牍。當有一根手指觸摸屏幕時就會產生一個UITouch對象案腺,當該手指移動時,該UITouch對象的屬性就會不斷更新康吵,當手指離開屏幕時劈榨,該UITouch對象就會消亡,所以其生命周期是從手指接觸屏幕開始晦嵌,到手指離開屏幕結束同辣。下面是相關屬性和方法:?
// 觸摸點所在的窗口?
@property(nonatomic,readonly,retain) UIWindow *window;?
// 觸摸點所在的視圖?
@property(nonatomic,readonly,retain) UIView *view;?
// 短時間內點按屏幕的次數,可以根據tapCount判斷單擊惭载、雙擊或多點擊?
@property(nonatomic,readonly) NSUInteger tapCount;?
// 觸摸事件產生或變化時的時間旱函,單位是s?
@property(nonatomic,readonly) NSTimeInterval timestamp;?
// 當前觸摸事件的狀態(tài)?
@property(nonatomic,readonly) UITouchPhase phase;?
// 觸摸點在view上的位置(左上角為原點),參數view為nil時描滔,返回觸摸點在UIWindow上的位置?
- (CGPoint)locationInView:(UIView *)view;?
// 前一個觸摸點的位置?
- (CGPoint)previousLocationInView:(UIView *)view;?
1.3.2棒妨、UIEvent?
UIEvent表示事件對象,記錄事件產生的時刻和類型含长,可以將短時間內發(fā)生的觸摸屏幕的多個UITouch視為一個事件券腔,看作一個特定的手勢或動作伏穆。UIEvent對象的生命周期也是從手指觸摸屏幕開始到手指離開屏幕結束(如果有多個手指,以這些手指觸屏引發(fā)事件開始纷纫,到任何一個手指離開屏幕導致事件結束為止)枕扫。下面是相關的屬性和方法:?
// 事件類型?
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);?
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);?
// 事件產生的時間?
@property(nonatomic,readonly) NSTimeInterval timestamp;?
// 觸摸對象的集合?
@property(nonatomic, readonly, nullable) NSSet*allTouches;?
// 獲取對應窗口上的觸摸對象的集合?
- (nullable NSSet*)touchesForWindow:(UIWindow *)window;?
// 獲取對應視圖上的觸摸對象的集合?
- (nullable NSSet*)touchesForView:(UIView *)view;?
// 獲取對應手勢識別器中的觸摸對象的集合?
- (nullable NSSet*)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);?
// - (nullable NSArray*)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);?
- (nullable NSArray *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
2、UIGestureRecognizer
? ? ? UIGestureRecognizer 是專門針對 UIView 的手勢識別器辱魁,用于識別并處理特定手勢铡原,一個手勢包含一個或多個 UITouch。其派生體系如下:
二商叹、事件傳遞過程
? ? ? iOS是依靠“事件響應鏈”進行事件傳遞的燕刻,所謂“事件響應鏈”是一個由許多 UIResponder 對象組成的鏈條,要搞清楚這個鏈條剖笙,先來看看APP的視圖結構卵洗,如下圖:
? ? ? APP的生成次序是:首先生成?AppDelegate &?UIApplication,然后添加 UIWindow弥咪,再添加 rootViewController(UIViewController)过蹂,最后 rootViewController 的 view?添加各個子 view,這樣就形成一個視圖層級關系聚至,事件響應鏈就依賴這種關系酷勺,那么一條響應鏈的結構順序就是:
AppDelegate ->?UIApplication ->?UIWindow ->?UIViewController -> 根view -> 子view1 -> 孫子view1 ->>>
在這條響應鏈中,前一個對象依次是后一個對象的下一個響應者(即nextresponder)扳躬,它們之間的這種關系是在 setRootViewController &&?addsubview && ?viewcontroller初始化的時候形成的脆诉。
比如執(zhí)行方法: [viewA addsubview:viewB]; ?后,
就會有:viewB.nextResponder =?viewA贷币。
? ? ? 可見每一個視圖的父視圖是唯一的击胜,每一個視圖的下一個響應者也是唯一的,但是每一個父視圖就可以有好多子視圖役纹,可以有好多上一個響應者偶摔,因此一個APP可以有多條事件響應鏈,最終形成一個樹形結構促脉。
? ? ? 下面來看一條事件響應鏈的響應過程辰斋,以上圖為例,當一個事件(UIEvent)發(fā)生時瘸味,系統(tǒng)首先會將其傳遞給最上層的響應者(通常是處于視圖層級最頂部對應位置的視圖)宫仗,這個響應者就是一條響應鏈中的一個節(jié)點,如果該響應者能夠處理事件硫戈,則處理該事件并終止事件傳遞锰什,如果不能處理,事件就會沿著這條響應鏈向上(向AppDelegate那個方向)傳遞,直到遇到能夠處理該事件的響應者汁胆,最終如果事件未能被處理就會被丟棄梭姓。下面是具體步驟:
(1) 事件(UIEvent)發(fā)生。
(2) initial view 嘗試處理嫩码,如果不能處理誉尖,則傳遞給其父視圖(superview)。
(3) 父視圖嘗試處理铸题,如果不能處理铡恕,則繼續(xù)傳遞給其父視圖。
(4) 父視圖嘗試處理丢间,如果不能處理探熔,則傳遞給它所在的視圖控制器。
(5) 視圖控制器嘗試處理烘挫,如果仍不能處理诀艰,則將事件傳遞給?UIWindow。
(6) UIWindow?嘗試處理饮六,如果不能處理其垄,則傳遞給 UIApplication。
(7) UIApplication?嘗試處理卤橄,如果不能處理绿满,則傳遞給?AppDelegate。
(8) AppDelegate?嘗試處理窟扑,如果也不能處理喇颁,則丟棄該事件。
需要注意一點:當 UIView 添加了手勢識別器辜膝,事件傳遞到該 view 的時候无牵,那么該 view 的手勢識別器會優(yōu)先處理該事件,比如:UIButton 添加點擊事件和tap手勢厂抖,tap手勢優(yōu)先處理事件(另外,如果 UIButton 重寫 touchesBegan:withEvent: 等方法時克懊,addTarget:action:forControlEvents: 方法無效)忱辅,并且如果上層視圖沒有添加手勢識別器的話,下層視圖的手勢事件任然會執(zhí)行谭溉。
? ? ? 上面說過墙懂,一個APP可能有好多條事件響應鏈,那么事件發(fā)生時扮念,執(zhí)行哪一條呢损搬?這就要歸功于它的原理事件分發(fā)機制(hit-Testing)。當手指觸摸屏幕時,UIKit 就會產生一個事件(UIEvent)對象巧勤,并把它添加到事件隊列中嵌灰,然后由 UIApplication 從事件隊列中取出首先分發(fā)給?UIWindow,從?UIWindow 開始進行分發(fā)颅悉,分發(fā)是依靠 UIView 的兩個分類方法?hitTest:withEvent: &?pointInside:withEvent: 以及?UIView?的?userInteractionEnabled沽瞭、hidden、alpha 屬性來進行的剩瓶,以下圖為例:
假如觸摸事件發(fā)生在 ViewE 區(qū)域內驹溃,事件分發(fā)機制將執(zhí)行如下步驟:
(1) 從 window 開始一直傳遞到 ViewA,此過程和下面過程一樣延曙,不作詳細說明豌鹤。
(2) ViewA 調用?hitTest:withEvent: 方法,該方法內部做了一些判斷枝缔,先檢查 ViewA 的 userInteractionEnabled=YES布疙、hidden=NO、alpha>0.01 三個條件是否滿足要求魂仍,然后調用 pointInside:withEvent: 方法判斷觸摸點是否在該 UIView 的區(qū)域內拐辽,當所有檢查合格后,事件分發(fā)給它的子視圖?ViewB &?ViewC擦酌,也就是 ViewB &?ViewC?調用?hitTest:withEvent: 方法俱诸。
(3) 假如 ViewB 在 ViewC 的頂部,那么?ViewB 先執(zhí)步驟(2)赊舶,不滿足要求睁搭,那么 ViewC 繼續(xù)執(zhí)步驟(2),滿足笼平,然后 ViewC?將事件分發(fā)給其子視圖?ViewD &?ViewE园骆,如果?ViewC 在 ViewB 的頂部,那么?ViewC 先執(zhí)行步驟(2)寓调,因滿足要求锌唾,固?ViewB 就會被忽略,不再執(zhí)行步驟(2)夺英。
(4)?ViewD 和?ViewE 的執(zhí)行步驟同步驟(3)晌涕,最后因為?ViewE 沒有子視圖,所以就成為第一響應者了痛悯。
點擊這里獲取驗證代碼