這里, 將帶你解析"從你的手指觸摸屏幕, 到App作出響應(yīng)"這段歷程中究竟發(fā)生了什么事.
什么是事件?
iOS中事件分為3大類 : 觸摸事件, 加速計(jì)事件和遠(yuǎn)程控制事件.
當(dāng)你的手指在手機(jī)屏幕上觸摸時(shí), 產(chǎn)生了一個(gè)事件.
當(dāng)你拿起手機(jī)搖一搖時(shí), 產(chǎn)生了一個(gè)事件...
那么, 什么對(duì)象能夠響應(yīng), 處理這些事件?
響應(yīng)者
只有繼承自UIResponder的對(duì)象才能接收并處理事件, 我們把之類對(duì)象稱之為'響應(yīng)者'. UIApplication, UIViewController, UIView都繼承自UIResponder, 因此他們都可以接收處理事件.
UIResponder內(nèi)部提供處理事件的方法有 :
觸摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
加速計(jì)事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠(yuǎn)程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
除了以上方法, 現(xiàn)在還有一個(gè)類, UIGestureRecognizer, 它是一個(gè)抽象類,使用它的子類能幫助我們輕松識(shí)別view上的各種手勢(shì).
UITapGestureRecognizer // 敲擊
UIPinchGestureRecognizer // 捏合,用于縮放
UIPanGestureRecognizer // 拖拽
UISwipeGestureRecognizer // 輕掃
UIRotationGestureRecognizer // 旋轉(zhuǎn)
UILongPressGestureRecognizer // 長(zhǎng)按
UITouch
當(dāng)你用一根手指觸摸屏幕時(shí), 會(huì)創(chuàng)建一個(gè)與之關(guān)聯(lián)的UITouch對(duì)象, 一個(gè)UITouch對(duì)象對(duì)應(yīng)一根手指. 在事件中可以根據(jù)NSSet中UITouch對(duì)象的數(shù)量得出此次觸摸事件是單指觸摸還是雙指多指等等.
UITouch幾個(gè)重要的屬性 :
// 觸摸產(chǎn)生時(shí)所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
// 觸摸產(chǎn)生時(shí)所處的視圖
@property(nonatomic,readonly,retain) UIView *view;
// 短時(shí)間內(nèi)點(diǎn)按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點(diǎn)擊
@property(nonatomic,readonly) NSUInteger tapCount;
// 記錄了觸摸事件產(chǎn)生或變化時(shí)的時(shí)間,單位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
// 當(dāng)前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase phase;
UITouch的兩個(gè)方法 (可用于view的拖拽)
/*
返回值表示觸摸在view上的位置
這里返回的位置是針對(duì)傳入的view的坐標(biāo)系(以view的左上角為原點(diǎn)(0, 0))
調(diào)用時(shí)傳入的view參數(shù)為nil的話,返回的是觸摸點(diǎn)在UIWindow的位置
*/
- (CGPoint)locationInView:(UIView *)view;
// 該方法記錄了前一個(gè)觸摸點(diǎn)的位置
- (CGPoint)previousLocationInView:(UIView *)view;
UIEvent
每產(chǎn)生一個(gè)事件, 就對(duì)應(yīng)產(chǎn)生一個(gè)UIEvent. UIEvent記錄著該事件產(chǎn)生的時(shí)間, 事件的類型等等.
UIEvent幾個(gè)重要的屬性 :
// 事件類型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
// 事件產(chǎn)生的時(shí)間
@property(nonatomic,readonly) NSTimeInterval timestamp;
事件的產(chǎn)生與傳遞
手機(jī)屏幕是一個(gè)2D的平面, 這上面有幾個(gè)view重疊在一起, 系統(tǒng)并不能分辨出你點(diǎn)擊的是哪一個(gè)view, 所以響應(yīng)你的觸摸事件的是這個(gè)應(yīng)用程序UIApplication, 而不是其中的一個(gè)UIView. 系統(tǒng)會(huì)將這次觸摸事件加入到由UIApplication管理的一個(gè)隊(duì)列中, 每次從中取出事件分發(fā)下去處理. 一般先發(fā)給keyWindow.
keyWindow會(huì)在視圖的層級(jí)結(jié)構(gòu)中找到一個(gè)最合適的控件來(lái)處理觸摸事件.
由于觸摸事件的傳遞方向是由父控件傳遞到子控件, 那何為最合適呢?
- 自己能響應(yīng)觸摸事件
- 觸摸點(diǎn)在自己身上
- 從后往前遍歷子控件, 重復(fù)上兩步
- 如果沒(méi)有符合條件的子控件, 那么就自己最合適處理
根據(jù)上圖來(lái)說(shuō)明的話
// 點(diǎn)擊了綠色的view:
UIApplication -> UIWindow -> 白色 -> 綠色
// 點(diǎn)擊了藍(lán)色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍(lán)色
// 點(diǎn)擊了黃色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍(lán)色 -> 黃色
兩個(gè)系統(tǒng)在尋找最合適view的時(shí)候使用到的方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
當(dāng)事件傳遞給控件的時(shí)候, 就會(huì)調(diào)用該方法, 去尋找最合適的view并返回
底層實(shí)現(xiàn)如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當(dāng)前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點(diǎn)在不在當(dāng)前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒(méi)有比自己更合適的view
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
該方法判斷觸摸點(diǎn)是否在控件身上, 是則返回YES, 否則返回NO
作用
可以使用以上兩個(gè)方法做到
- 指鹿為馬(明明點(diǎn)擊的是B視圖, 卻由A視圖來(lái)響應(yīng)事件)
- 穿透某控件點(diǎn)擊被覆蓋的下一層控件
- 讓父控件frame之外的子控件響應(yīng)觸摸事件
響應(yīng)者鏈條
當(dāng)找到最合適的響應(yīng)者之后, 便會(huì)調(diào)用控件相應(yīng)的touches方法來(lái)作具體處理. 然而這些方法默認(rèn)是不處理, 并將該事件隨著響應(yīng)者鏈條往回傳遞, 交給上一個(gè)響應(yīng)者來(lái)處理. (即調(diào)用super的touches方法)
很詭異吧, 好不容易找到了你, 你卻跟老子說(shuō)你不干活, 給前面找你的人干?
其實(shí)蘋果這樣設(shè)計(jì)是為了讓一個(gè)事件能夠被多個(gè)對(duì)象處理!
那么問(wèn)題來(lái)了~ 誰(shuí)是上一個(gè)響應(yīng)者? 如圖所示 :
1. 如果view的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父視圖
2. 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件傳遞給window對(duì)象進(jìn)行處理
3. 如果window對(duì)象也不處理,則其將事件或消息傳遞給UIApplication對(duì)象
4. 如果UIApplication也不能處理該事件或消息,則將其丟棄
有趣的是,當(dāng)調(diào)用UIControl的addTarget:action:forControlEvents:時(shí),如果target設(shè)為nil,系統(tǒng)則會(huì)從UIControl本身開始沿著響應(yīng)鏈往上找,直到找到有一個(gè)響應(yīng)者響應(yīng)了該事件為止,否則該事件則被丟棄。