1.事件傳遞的流程:

2.事件傳遞圖示

如果想讓某個(gè)view不能處理事件(或者說(shuō),事件傳遞到某個(gè)view那里就斷了),那么可以通過(guò)剛才提到的三種方式。比如,設(shè)置其userInteractionEnabled = NO;那么傳遞下來(lái)的事件就會(huì)由該view的父控件處理。
例如,不想讓藍(lán)色的view接收事件,那么可以設(shè)置藍(lán)色的view的userInteractionEnabled = NO;那么點(diǎn)擊黃色的view或者藍(lán)色的view所產(chǎn)生的事件,最終會(huì)由橙色的view處理,橙色的view就會(huì)成為最合適的view。
所以,不管視圖能不能處理事件,只要點(diǎn)擊了視圖就都會(huì)產(chǎn)生事件,關(guān)鍵在于該事件最終是由誰(shuí)來(lái)處理!也就是說(shuō),如果藍(lán)色視圖不能處理事件,點(diǎn)擊藍(lán)色視圖產(chǎn)生的觸摸事件不會(huì)由被點(diǎn)擊的視圖(藍(lán)色視圖)處理!
注意:如果設(shè)置父控件的透明度或者h(yuǎn)idden,會(huì)直接影響到子控件的透明度和hidden。如果父控件的透明度為0或者h(yuǎn)idden = YES,那么子控件也是不可見(jiàn)的!
3.流程描述:
- 我們點(diǎn)擊屏幕產(chǎn)生觸摸事件,系統(tǒng)會(huì)將這個(gè)事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中,UIApplication會(huì)從消息隊(duì)列里取事件分發(fā)下去,首先傳給UIWindow
- 在UIWindow中就會(huì)調(diào)用 hitTest:withEvent: 方法去返回一個(gè)最終響應(yīng)的視圖
- 在hitTest:withEvent方法中就會(huì)去調(diào)用第二個(gè)方法 pointInside:withEvent: 去判斷當(dāng)前點(diǎn)擊的point是否在UIWindow范圍內(nèi),如果是的話(huà),就會(huì)去遍歷它的子視圖來(lái)查找最終響應(yīng)的子視圖
同級(jí)view的遍歷方式是使用倒序的方式來(lái)遍歷子視圖,也就是說(shuō)最后添加的子視圖會(huì)最先遍歷,在每一個(gè)視圖中都去回去調(diào)用它的 hitTest:withEvent: 方法,可以理解成為是一個(gè)遞歸調(diào)用- 最終會(huì)返回一個(gè)響應(yīng)視圖,如果返回的視圖有值,那么這個(gè)視圖就作為最終響應(yīng)視圖,結(jié)束整個(gè)事件傳遞,如果沒(méi)有值,那么就會(huì)將UIWindow作為響應(yīng)值
4.hitTest:withEvent: 方法的流程
- 首先會(huì)判斷當(dāng)前視圖的hiden屬性、是否可以交互及透明度是否大于0.01,如果滿(mǎn)足條件則進(jìn)入下一步,否則返回nil
- 然后調(diào)用
pointInside:withEvent:方法來(lái)判斷這個(gè)點(diǎn)是否在當(dāng)前范圍內(nèi),如果滿(mǎn)足條件進(jìn)入下一步,否則返回nil- 返回以
倒序的方式遍歷它的子視圖,在每一個(gè)子視圖中取調(diào)用hitTest:withEvent:,如果有一個(gè)子視圖返回了一個(gè)最終的響應(yīng)視圖,就將這個(gè)視圖返回給調(diào)用方,結(jié)束流程。如果全部遍歷完成都沒(méi)有找到一個(gè)最終響應(yīng)視圖,因?yàn)辄c(diǎn)擊位置在當(dāng)前視圖范圍內(nèi),就將當(dāng)前視圖作為最終響應(yīng)視圖返回
二、視圖響應(yīng)鏈
2.1 事件的分類(lèi)
- multitouch events
- motion events
- remote control events
- Multitouch Events: 所謂的多點(diǎn)觸摸事件,非常好理解,即用戶(hù)觸摸屏幕交互產(chǎn)生的事件類(lèi)型。
- Motion Events: 所謂的移動(dòng)事件。是指用戶(hù)在搖晃,移動(dòng)和傾斜手機(jī)的時(shí)候產(chǎn)生的事件成為移動(dòng)事件。這類(lèi)事件依賴(lài)于iPhone手機(jī)里面的加速計(jì),陀螺儀等傳感器。
- Remote Control Events:所謂的遠(yuǎn)程控制事件。這個(gè)事件從名稱(chēng)上面看,不太好理解。但其實(shí),這個(gè)事件指的是用戶(hù)在操作多媒體的時(shí)候產(chǎn)生的事件。比如,播放音樂(lè)、視頻等。
2.2 什么是Responder
- Responder的屬性和方法,從下面的方法可以看出UIResponder可以處理Touchevent,motionevent,remote control event
- (UIResponder )nextResponder;
- (BOOL)canBecomeFirstResponder; // default is NO
- (BOOL)becomeFirstResponder;
// Touch Event
- (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;
// Motion Event
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
// Remote Control Event
(void)remoteControlReceivedWithEvent:(UIEvent)event NS_AVAILABLE_IOS(4_0);
注意有個(gè)很重要的方法,nextResponder,很明顯可以看出來(lái)響應(yīng)是一條鏈表結(jié)構(gòu),通過(guò)nestResponder找到下一個(gè)responder。這里是從第一個(gè)responder開(kāi)始通過(guò)nextresponder傳遞事件,直到有responder響應(yīng)了事件就停止傳遞;如果傳到最后一個(gè)responder都沒(méi)有被響應(yīng),那么該事件就被拋棄。
那么,誰(shuí)是第一個(gè)resopnder呢? responder是怎么響應(yīng)的呢?responder響應(yīng)后為什么不往下傳遞了呢?稍后會(huì)一一回答
2.3 UIResponder的衍生類(lèi):
UIApplication UIViewController UIView都是繼承UIResponder,都可以傳遞和響應(yīng)事件

那么就可以這么理解,我們看到的一個(gè)界面,可能是由一個(gè)UIApplication和多個(gè) UIViewController UIView組成,他們都是responder,他們一起組成了響應(yīng)連。每次發(fā)生觸摸事件,該事件就在這條響應(yīng)鏈里傳遞
2.4 誰(shuí)是第一個(gè)responder?
拿touchevent事件舉例,一般情況下(因?yàn)橛虚_(kāi)放可以主動(dòng)設(shè)置firstresponder),當(dāng)前正在點(diǎn)擊的視圖對(duì)象就是first responder。
2.5 如何尋找first responder?
#事件傳遞的兩個(gè)核心方法
#第一個(gè)方法返回一個(gè)UIView,是用來(lái)尋找哪一個(gè)視圖來(lái)響應(yīng)這個(gè)事件
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
#第二個(gè)方法是用來(lái)判斷某一個(gè)點(diǎn)擊的位置 是否在視圖范圍內(nèi),如果在就返回YES
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
hitTest和pointinside是系統(tǒng)遍歷尋找firstresponder的方法。最終返回的view就是當(dāng)前觸摸的first responder
2.6 hitTest遍歷尋找first responder 的規(guī)則:
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:消息,所有子視圖的遍歷順序是從top到bottom,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢;那么,后面的addsubview進(jìn)來(lái)的子view就會(huì)優(yōu)先被選中為first responder
4)若第一次有子視圖返回非空對(duì)象,則hitTest:withEvent:方法返回此對(duì)象,處理結(jié)束;
注意:子view返回非空對(duì)象,若該子view還擁有自己的subviews,那么步驟3是個(gè)遞歸遍歷。
5)若所有子視圖都返回非,則hitTest:withEvent:方法返回自身(self)。

- ios沒(méi)有源碼,下面是模擬源碼寫(xiě)的
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = [super hitTest:point withEvent:event];
CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
if ([underButton pointInside:buttonPoint withEvent:event]) {
return underButton;
}
return result;
}
從上面規(guī)則可以知道,視圖超出父視圖的區(qū)域是不會(huì)參與到遍歷的,這是沒(méi)有意義的計(jì)算。加上這個(gè),一共有4種情況的view是不會(huì)參與到遍歷的
1)隱藏(hidden=YES)的視圖
2)禁止用戶(hù)操作(userInteractionEnabled=NO)的視圖
3)alpha<0.01的視圖
4)視圖超出父視圖的區(qū)域
也就是說(shuō)這四種情況的視圖,以及他們的子視圖是不會(huì)成為responder的。
-
下面通過(guò)一個(gè)比較直觀的圖形來(lái)講述上面的規(guī)則
image.png
假設(shè)用戶(hù)點(diǎn)擊了視圖D:
- 檢測(cè)到點(diǎn)擊坐標(biāo)在View A范圍之內(nèi)。
- 繼續(xù)檢測(cè)點(diǎn)擊范圍是否在其子視圖B,C范圍內(nèi)。發(fā)現(xiàn)點(diǎn)擊范圍在視圖C范圍內(nèi),則忽略掉B視圖及其子視圖分支。
- 繼續(xù)檢測(cè)點(diǎn)擊范圍是否在其子視圖D范圍內(nèi),如果是,則用戶(hù)當(dāng)前視圖即為視圖D。如果不是,繼續(xù)檢測(cè)其子視圖。
- 總結(jié):iOS系統(tǒng)會(huì)從父視圖向子視圖依次查找,直到找到點(diǎn)擊范圍在當(dāng)前視圖邊界范圍以?xún)?nèi)。如果點(diǎn)擊范圍在某子視圖范圍內(nèi),并且沒(méi)有了子視圖,則該視圖即為當(dāng)前點(diǎn)擊視圖。如果點(diǎn)擊范圍在某子視圖范圍之內(nèi),并且不在其子視圖范圍之內(nèi),則點(diǎn)擊視圖即為當(dāng)前點(diǎn)擊視圖。
- 總結(jié):事件的傳遞和響應(yīng)
從上面可以看出,事件的傳遞方向是(hittest就是事件的傳遞):
UIApplication -> UIWindow ->ViewController-> UIView -> initial view
而Responder傳遞方向是(還記得nextResponder嗎):
Initial View -> Parent View -> ViewController -> Window -> Application
如果最終傳遞到Application對(duì)象,依然沒(méi)有對(duì)事件作出響應(yīng),事件就會(huì)被舍棄掉。
- 怎么樣才算是對(duì)事件做出了響應(yīng)呢
在事件的響應(yīng)中,如果某個(gè)控件實(shí)現(xiàn)了touches...方法,則這個(gè)事件將由該控件來(lái)接受,如果調(diào)用了[supertouches….];就會(huì)將事件順著響應(yīng)者鏈條往上傳遞,傳遞給上一個(gè)響應(yīng)者;接著就會(huì)調(diào)用上一個(gè)響應(yīng)者的touches….方法
- 點(diǎn)擊、搖動(dòng)、滑動(dòng)、旋轉(zhuǎn)等會(huì)被系統(tǒng)封裝成UIEvent,放到事件隊(duì)列里等待UIApplication去取,然后尋找響應(yīng)者,找到對(duì)應(yīng)的方法并執(zhí)行的過(guò)程就是響應(yīng)。
通過(guò)上述的傳遞事件會(huì)找到第一響應(yīng)者,這時(shí)就用第一響應(yīng)者來(lái)響應(yīng)。
-
如果第一響應(yīng)者不響應(yīng),不響應(yīng)的傳遞流程示圖
視圖響應(yīng)鏈.png
點(diǎn)擊當(dāng)前視圖initial View - initial View的父視圖view - view controller - UIWindow - UIApplication
- 通過(guò)代碼來(lái)展示
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@ touch begin", self.class);
UIResponder *next = [self nextResponder];
while (next) {
NSLog(@"下一個(gè)響應(yīng)者%@",next.class);
next = [next nextResponder];
}
}
-
打印結(jié)果:
image.png - 如果傳遞到UIApplication也沒(méi)有響應(yīng),則這個(gè)事件作廢.
轉(zhuǎn)自簡(jiǎn)書(shū):http://www.itdecent.cn/p/94b0539b2178
轉(zhuǎn)自博客:ios事件傳遞和響應(yīng)機(jī)制


