前言(鋪墊一下)
當(dāng)發(fā)生事件時(shí)必須知道由誰(shuí)來響應(yīng)事件。
所有事件響應(yīng)的類都是UIResponder的子類震缭,
響應(yīng)鏈?zhǔn)且粋€(gè)由不同對(duì)象組成的層次結(jié)構(gòu),
其中的每個(gè)對(duì)象將依次獲得響應(yīng)事件消息的機(jī)會(huì)党涕。
當(dāng)發(fā)生事件時(shí) 事件首先被發(fā)送給第一響應(yīng)者巡社,
第一響應(yīng)者往往是事件發(fā)生的視圖,也就是用戶觸摸屏幕的地方肥荔。
事件將沿著響應(yīng)者鏈一直向下傳遞朝群,直到被接受并做出處理。
一般來說第一響應(yīng)者是個(gè)視圖對(duì)象或者其子類對(duì)象誉帅,當(dāng)其被觸摸后事件被交由它處理堵第,如果它不處理事件就會(huì)被傳遞給它的視圖控制器對(duì)象viewcontroller(如果存在)踏志,然后是它的父視圖(superview)對(duì)象(如果存在)针余,以此類推直到頂層視圖凄诞。接下來會(huì)沿著頂層視圖(top view)到窗口(UIWindow對(duì)象)再到程序(UIApplication對(duì)象)。
如果整個(gè)過程都沒有響應(yīng)這個(gè)事件伪朽,該事件就被丟棄烈涮。
在響應(yīng)者鏈中只要由對(duì)象處理事件坚洽,事件就停止傳遞。
典型的 事件響應(yīng)鏈 路線圖:
由離用戶最近的view向系統(tǒng)傳遞鞍盗。
First Responser -- >View -- >SuperView...-- >The Window -- >The Application -- > App Delegate
響應(yīng)者鏈流程經(jīng)常被委托(delegation)打斷跳昼,
一個(gè)對(duì)象(通常是視圖)可能將響應(yīng)工作委托給另一個(gè)對(duì)象來完成(通常是視圖控制器ViewController),所以 做事件響應(yīng)時(shí)在ViewController中必須實(shí)現(xiàn)相應(yīng)協(xié)議來實(shí)現(xiàn)事件委托欣除。
iOS中的UIResponder類,定義了響應(yīng)者對(duì)象的所有方法。UIApplication谱煤、UIView等類都繼承了UIResponder類室叉,UIWindow和UIKit中的控件因?yàn)槔^承了UIView,所以也間接繼承了UIResponder類恼除,這些類的實(shí)例都可以當(dāng)作響應(yīng)者豁辉。
下圖網(wǎng)絡(luò)出處:https://www.cnblogs.com/Julday/archive/2019/12/30/12119536.html
iOS事件的類型
iOS用戶操作設(shè)備的方式主要有三種:觸摸屏幕现使、晃動(dòng)設(shè)備努咐、通過遙控設(shè)施控制設(shè)備渗稍。
對(duì)應(yīng)的事件類型如下:
1竿屹、觸屏事件 (Touch Event)
2、運(yùn)動(dòng)事件 (Motion Event)
3力惯、遠(yuǎn)端控制事件(Remote-Control Event)
慣例我們就以觸屏事件Touch Event為例說明在Cocoa Touch框架中事件的處理流程哮缺。
響應(yīng)鏈(Responder Chain)
響應(yīng)者對(duì)象(Responder Object)指的是有響應(yīng)和處理事件能力的對(duì)象尝苇。
響應(yīng)鏈 就是由一系列的 響應(yīng)者對(duì)象 構(gòu)成的一個(gè)層次結(jié)構(gòu)糠溜。
UIResponder是所有響應(yīng)對(duì)象的基類,在UIResponder類中定義了處理上述各種事件的接口谋竖。我們熟悉的UIApplication豹芯、 UIViewController铁蹈、UIWindow和所有繼承自UIView的UIKit類都直接或間接的繼承自UIResponder握牧,所以它們的實(shí)例都是可以構(gòu)成響應(yīng)者鏈的響應(yīng)者對(duì)象沿腰。
上圖可見 響應(yīng)者鏈的一些特點(diǎn):
1习蓬、響應(yīng)者鏈通常是由視圖(UIView)構(gòu)成的;
2枫慷、視圖的下一個(gè)響應(yīng)者是它de視圖控制器UIViewController(當(dāng)然如果有它的話)或听,然后再轉(zhuǎn)給它的父視圖Super View;
3萌抵、視圖控制器 的下一個(gè)響應(yīng)者shi其管理的視圖的父視圖;
4讨永、單例的窗口UIWindow的內(nèi)容視圖將指向窗口本身作為它的下一個(gè)響應(yīng)者卿闹;
5锻霎、單例的應(yīng)用UIApplication是一個(gè)響應(yīng)者鏈的終點(diǎn)旋恼,它的下一個(gè)響應(yīng)者指向nil冰更,結(jié)束整個(gè)循環(huán)舟铜。
Cocoa應(yīng)用可以有多個(gè)UIWindow對(duì)象。
Cocoa Touch應(yīng)用只有一個(gè)UIWindow對(duì)象涣觉,整個(gè)響應(yīng)者鏈比較簡(jiǎn)單官册。
事件分發(fā)(Event Delivery)
第一響應(yīng)者First responder指的是當(dāng)前接受觸摸的響應(yīng)者對(duì)象(通常是一個(gè)UIView對(duì)象)膝宁,即當(dāng)前該對(duì)象正在與用戶交互员淫,它是響應(yīng)者鏈的開端介返。
事件傳遞的最終目的:找出一個(gè)能處理并響應(yīng)事件的對(duì)象(第一響應(yīng)者First responder)
如何尋找第一響應(yīng)者(事件傳遞的過程)
事件傳遞鏈:
由系統(tǒng)向離用戶最近的view傳遞圣蝎。
UIKit –> active app's event queue –> window –> root view –> …… –> lowest view
1、當(dāng)iOS程序發(fā)生觸摸事件后关面,系統(tǒng)會(huì)利用Runloop將事件加入到UIApplication的任務(wù)隊(duì)列中等太,具體過程可以參考深入理解RunLoop
2辛燥、UIApplication分發(fā)觸摸事件到UIWindow挎塌,然后UIWindow依次向下分發(fā)給UIView
3榴都、UIView調(diào)用hitTest:withEvent:
方法看看自己能否處理事件竿音,以及觸摸點(diǎn)是否在自己上面春瞬。
4宽气、如果滿足條件萄涯,就遍歷UIView上的子控件涝影。重復(fù)上面的動(dòng)作。
5伯襟、直到找到最頂層的一個(gè)滿足條件(既能處理觸摸事件逗旁,觸摸點(diǎn)又在上面)的子控件,此子控件就是我們需要找到的第一響應(yīng)者英古。
hitTest:withEvent:的處理流程
(上面的查找其實(shí)就是由該方法遞歸調(diào)用實(shí)現(xiàn)的)
1、首先調(diào)用當(dāng)前視圖的pointInside:withEvent:方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi)唠叛;
2艺沼、若返回NO,則hitTest:withEvent:返回nil;
3调鲸、若返回YES,則向當(dāng)前視圖的所有子視圖(subviews)發(fā)送hitTest:withEvent:消息藐石,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數(shù)組的末尾向前遍歷办素,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢性穿;
4吗坚、若第一次有子視圖返回非空對(duì)象,則hitTest:withEvent:方法返回此對(duì)象谋减,處理結(jié)束出爹;
5、如所有子視圖都返回非梢为,則hitTest:withEvent:方法返回自身(self)。
hitTest:withEvent:方法的偽代碼大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.userInteractionEnabled || !self.hidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint subPoint = [subView convertPoint:point fromView:self];
UIView *bestView = [subView hitTest:subPoint withEvent:event];
if (bestView) {
return bestView;
}
}
return self;
}
return nil;
}
事件的響應(yīng)流程
通過上面的 hitTest:withEvent: 尋找到第一響應(yīng)者后,需要逆著尋找第一響應(yīng)者的方向(從第一響應(yīng)者->UIApplication)來響應(yīng)事件。
流程如下
1.首先通過 hitTest:withEvent: 確定第一響應(yīng)者,以及相應(yīng)的響應(yīng)鏈
2.判斷第一響應(yīng)者能否響應(yīng)事件芋哭,如果第一響應(yīng)者能進(jìn)行響應(yīng)則事件在響應(yīng)鏈中的傳遞終止豌习。如果第一響應(yīng)者不能響應(yīng)則將事件傳遞給 nextResponder也就是通常的superview進(jìn)行事件響應(yīng)
3.如果事件繼續(xù)上報(bào)至UIWindow并且無法響應(yīng),它將會(huì)把事件繼續(xù)上報(bào)給UIApplication
4.如果事件繼續(xù)上報(bào)至UIApplication并且也無法響應(yīng)稚失,它將會(huì)將事件上報(bào)給其Delegate
5.如果最終事件依舊未被響應(yīng)則會(huì)被系統(tǒng)拋棄
哪些視圖不響應(yīng)呢吸占?
hidden = YES 視圖被隱藏
userInteractionEnabled = NO 不接受響應(yīng)事件
alpha <= 0.01,透明視圖不接收響應(yīng)事件
子視圖超出父視圖范圍
需響應(yīng)視圖被其他視圖蓋住
是否重寫了其父視圖以及自身的hitTest方法
是否重寫了其父視圖以及自身的pointInside方法
應(yīng)用場(chǎng)景
方形按鈕點(diǎn)擊四角無效,點(diǎn)擊中間的圓形區(qū)域有效件蚕。
核心思路:在pointInside: withEvent:方法中修改對(duì)應(yīng)的區(qū)域排作。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果控件不允許與用用戶交互,那么返回nil
if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
return nil;
}
//判斷當(dāng)前視圖是否在點(diǎn)擊范圍內(nèi)
if ([self pointInside:point withEvent:event]) {
//遍歷當(dāng)前對(duì)象的子視圖(倒序)
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//坐標(biāo)轉(zhuǎn)換蕾久,把當(dāng)前坐標(biāo)系上的點(diǎn)轉(zhuǎn)換成子控件坐標(biāo)系上的點(diǎn)
CGPoint convertPoint = [self convertPoint:point toView:obj];
//調(diào)用子視圖的hitTest方法履因,判斷自己的子控件是不是最適合的View
hit = [obj hitTest:convertPoint withEvent:event];
//如果找到了就停止遍歷
if (hit) *stop = YES;
}];
//返回當(dāng)前的視圖對(duì)象
return hit?hit:self;
}else {
return nil;
}
}
// 該方法判斷觸摸點(diǎn)是否在控件身上栅迄,是則返回YES毅舆,否則返回NO憋活,point參數(shù)必須是方法調(diào)用者的坐標(biāo)系
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;
//判斷是否在圓形區(qū)域內(nèi)
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}