這里主要講解記錄下用戶觸摸點擊手機屏幕后產(chǎn)生的事件是如何派發(fā)傳遞的,如何查找到適合響應(yīng)事件的第一響應(yīng)者控件,以及找到響應(yīng)者后事件是如何通過響應(yīng)鏈向下傳遞的,直到事件被接收并做出具體處理或被廢棄。事件響應(yīng)鏈也是面試中經(jīng)常會被問起的知識點!
一、相關(guān)概念
- 第一響應(yīng)者:
第一響應(yīng)者一般指的是用戶當(dāng)前觸摸的響應(yīng)者對象,表示當(dāng)前該對象正在與用戶交互,第一響應(yīng)者是響應(yīng)者鏈的開端。
響應(yīng)者鏈和事件分發(fā)傳遞的使命都是找出第一響應(yīng)者
- 響應(yīng)者對象:
具有響應(yīng)和處理iOS事件能力的對象,也就是繼承UIResponder的類的對象。我們常用的UIApplication、UIWindow、UIViewController、UIView、UIScene(iOS13以后)都是繼承或間接UIResponder類,所以他們的實例對象都可以成為響應(yīng)者對象。
類的繼承關(guān)系:
- 響應(yīng)者鏈:
由多個不同響應(yīng)者對象鏈接起來構(gòu)成的一個鏈條;
響應(yīng)者鏈可以看做是鏈表,整體是一個樹,因為每個節(jié)點都是一個響應(yīng)者對象,每個響應(yīng)者對象都存有指向下一個響應(yīng)者的指針nextResponder,可以通過nestResponder找到下一個responder,直到找到第一響應(yīng)者響應(yīng)了事件就會停止傳遞,如果最終沒有響應(yīng)者響應(yīng)事件,那么該事件就會被廢棄。
二、iOS中的事件類型:
iOS中事件主要分為三大類:
1、Touch Event (觸摸事件)
解釋:用戶觸摸屏幕產(chǎn)生的交互事件
2、Motion Event (運動事件)
解釋:運動事件也叫做加速計事件,這類事件是依賴手機里的加速計、陀螺儀等硬件傳感器實現(xiàn)的。用戶在搖晃手機、傾斜手機的售后就會產(chǎn)生這類事件。可用于屏幕轉(zhuǎn)屏監(jiān)控。
3、Remote-ControlEvent(遠(yuǎn)程控制事件)
解釋:這個事件指的是用戶在操作多媒體的時候產(chǎn)生的事件。例如播放音樂時后臺播放控制
三、如何控制控件能不能響應(yīng)事件:
- 設(shè)置不允許交互:設(shè)置控件的userInteractionEnabled = NO;
- 設(shè)置控件隱藏:將控件的hidden設(shè)置為Yes隱藏控件;
- 設(shè)置透明度:設(shè)置控件的透明度alpha<0.01,放alpha的值在0.0~0.01之間時控件為透明;
- 超出父控件響應(yīng)區(qū)域
注意:如果view被設(shè)置為透明,那么會直接影響其子View的透明度;如果view無法響應(yīng)事件那么這個view上的所有SubView都不可響應(yīng)事件,也就是如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件。
四、事件的產(chǎn)生和分發(fā)傳遞:
測試展示圖
- 事件是如何產(chǎn)生的?
當(dāng)用戶觸摸屏幕時,系統(tǒng)會檢測到屏幕上的壓力感知到觸摸事件,iOS系統(tǒng)檢測到觸摸操作后會將這個事件打包成一個UIEvent對象,并將該事件加入到一個由UIApplication管理的事件隊列中,然后UIApplication會從事件隊列中取出觸摸事件并傳遞給UIWindow處理,keyWindow會使用hitTest:withEvent:方法尋找一個最合適的響應(yīng)者來處理事件,一般尋找到的適合處理事件的控件是touch操作初始點的視圖,找到合適第一響應(yīng)者的視圖控件后,就會調(diào)用該視圖控件的touches方法來處理具體的事件,這個過程稱之為hit-test。
- 處理事件的方法:
UIResponder內(nèi)部提供了以下方法來處理事件觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
- 事件是如何傳遞的?
事件的傳遞是由父控件向子控件傳遞的,例如上面的view層次圖,viewA、viewB、viewE被添加到rootView中,viewC、viewD是viewB的子view。加入用戶點擊viewC的時間傳遞鏈?zhǔn)?/p>
傳遞方向:由底層系統(tǒng)向可以響應(yīng)事件的控件傳遞
UIKit→UIApplication的事件隊列→keyWindow→rootView→一些列subView→事件響應(yīng)view
- 如何查找到合適的事件第一響應(yīng)者?
主要方法:
- (nullable UIView )hitTest:(CGPoint)point withEvent:(nullable UIEvent )event;
注:只要事件傳遞給一個控件,那么這個控件就會調(diào)用自己的hitTest:withEvent:方法。他的作用是尋找并返回適合響應(yīng)處理事件的第一響應(yīng)者。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
注:作用是判斷點在不在當(dāng)前view上(方法調(diào)用者的坐標(biāo)系上)如果返回YES,代表點在方法調(diào)用者的坐標(biāo)系上;返回NO代表點不在方法調(diào)用者的坐標(biāo)系上,那么方法調(diào)用者也就不能處理事件。
流程圖:
- 主窗口接收到應(yīng)用程序傳遞過來的事件后,首先判斷自己能否接手觸摸事件。如果能,那么在判斷觸摸點在不在窗口的范圍內(nèi)
- 如果觸摸點也在窗口身上,那么窗口會從后往前遍歷自己的子控件,遍歷自己的子控件只是為了尋找出來最合適的view;
- 遍歷到每一個子控件后,又會重復(fù)上面的兩個步驟。將傳遞事件給子控件,先判斷子控件能否接受事件,再判斷觸摸點在不在子控件的范圍中;
- 如此循環(huán)遍歷子控件,直到找出合適響應(yīng)事件的第一響應(yīng)者,如果沒有更合適的子控件,那么自己就成為最合適的view。
- hitTest:withEvent方法中如何處理的?
- 首先判斷當(dāng)前視圖是否可響應(yīng)事件,也就是判斷當(dāng)前視圖的是否可交互狀態(tài)、隱藏狀態(tài)、透明度;
- 如果當(dāng)前視圖允許響應(yīng)觸摸事件,則調(diào)用當(dāng)前視圖的 pointInside:withEvent: 方法判斷觸摸點是否在當(dāng)前視圖內(nèi) ;
- 若pointInside:withEvent:返回NO,則 hitTest:withEvent: 返回 nil ;
- 若pointInside:withEvent:返回 YES,則向當(dāng)前視圖的所有子視圖發(fā)送 hitTest:withEvent: 消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從 subviews 數(shù)組的末尾向前遍歷 ,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;
- 若第一次有子視圖返回非空對象,則 hitTest:withEvent: 方法返回此對象,處理結(jié)束;如所有子視圖都返回 nil,則 hitTest:withEvent: 方法返回該視圖自身 ;
- 找到合適的第一響應(yīng)者后,就會調(diào)用該控件的touches系列方法處理具體的事件,如果找不到第一響應(yīng)者就不會調(diào)用touches方法
- 查找響應(yīng)者實例
以測試展示圖為例,假設(shè)用戶點擊viewC后的處理流程
- rootView為window的根視圖,窗口會首先對rootView進(jìn)行hit-Test,判斷結(jié)果為用戶點擊位置在rootView的范圍內(nèi);
- 繼續(xù)檢測rootView的子控件(viewA,viewB,viewE)相應(yīng)的調(diào)用自己的hit-Test方法,檢測到viewA、viewE的pointInside:withEvent:返回NO,則點擊范圍不在viewA、viewE內(nèi),對應(yīng)的hitTest:withEvent:返回nil,這時rootView繼續(xù)檢測viewB的hit-Test方法,viewB的pointInside:withEvent:返回YES,確定點擊范圍在viewB內(nèi);
- 這時viewB內(nèi)存在viewC和viewD兩個子控件,viewD在viewC之后添加到viewB的subViews中,因此優(yōu)先檢測viewD的hit-Test方法,viewD的pointInside:withEvent:返回NO,對應(yīng)的hitTest:withEvent:返回nil,說明點擊不在viewD內(nèi),viewD及其子控件都不可響應(yīng)事件。因此需要回溯檢測viewC的hit-Test方法;
- viewC的pointInside:withEvent:返回YES,說明點擊范圍在viewC范圍內(nèi),由于viewC沒有子控件,也可以理解為viewC的子控件hit-Test返回了nil;
- 因此viewC的hitTest:withEvent:將會返回viewC,viewB的hitTest:withEvent:返回viewC,rootViewhitTest:withEvent:將會返回viewC;
- 至此,本次點擊事件的第一響應(yīng)者就通過響應(yīng)者鏈的事件分發(fā)邏輯找到了
注意:如果最終hit-test沒有找到第一響應(yīng)者,或者第一響應(yīng)者沒有處理該事件,則該事件會沿著響應(yīng)者鏈向上回溯,如果UIWindow實例和UIApplication實例都不能處理該事件,則該事件會被丟棄;
- hitTest:withEvent:方法底層實現(xiàn)
// 什么時候調(diào)用:只要事件一傳遞給一個控件,那么這個控件就會調(diào)用自己的這個方法
// 作用:尋找并返回最合適的view
// UIApplication -> [UIWindow hitTest:withEvent:]尋找最合適的view告訴系統(tǒng)
// point:當(dāng)前手指觸摸的點
// point:是方法調(diào)用者坐標(biāo)系上的點
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判斷下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判斷下點在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷子控件數(shù)組
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 獲取子控件
UIView *childView = self.subviews[I];
// 坐標(biāo)系的轉(zhuǎn)換,把窗口上的點轉(zhuǎn)換為子控件上的點
// 把自己控件上的點轉(zhuǎn)換成子控件上的點
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合適的view
return fitView;
}
}
// 4.沒有找到更合適的view,也就是沒有比自己更合適的view
return self;
}
五、事件響應(yīng)
- 響應(yīng)鏈的傳遞方向
由是第一響應(yīng)者的控件向系統(tǒng)傳遞
事件響應(yīng)view→superView→rootVIew→viewController→window→Application→AppDelegate
- 響應(yīng)者鏈的關(guān)系圖:
解釋說明:
1、響應(yīng)者鏈?zhǔn)怯啥鄠€響應(yīng)者對象構(gòu)成的鏈條,每個響應(yīng)者對象必須是繼承UIResponder類的子類;
2、如果View是控制器VC的View,那么VC就是view的nextUIResponder;
3、如果View不是控制器VC的View,那么此View的superView為當(dāng)前view的nextUIResponder;
4、視圖控制器VC的nextUIResponder是控制器view(VC.View)的superView,即VC.nextUIResponder = VC.View.superView,如下圖;
5、如果在視圖層都不能處理事件,則將事件傳遞個UIWindow進(jìn)行處理;
6、Window的nextResponder是UIApplication,如果window也不處理事件,則將事件傳遞給UIApplication;
7、UIApplication的nextResponder是AppDelegate,如果UIApplication也不能處理該事件,則將此事件丟棄
說明示圖:
輸出的log 顯示VC.nextUIResponder = VC.View.superView
六、總結(jié)
- 當(dāng)用戶點擊頁面上一個view的時候,系統(tǒng)只是檢測到用戶點擊觸摸了屏幕,而此時無法確認(rèn)用戶觸摸的view控件,因此需要根據(jù)事件分發(fā)傳遞的邏輯尋找到可以響應(yīng)事件的第一響應(yīng)者控件;
- 如果需要處理特殊的需求,例如單擊不規(guī)則按鈕事件、點擊事件穿透等問題時可以重寫主要方法hitTest:withEvent:和pointInside:withEvent:來處理;
- 發(fā)生了觸摸或其他事件后,系統(tǒng)將事件打包成UiEvent發(fā)送到UIApplication管理的事件隊列中,UIApplication從隊列中取出最前面的事件分發(fā)下去;
- 如果找到了合適處理事件的控件,會調(diào)用此控件的touchs系列方法,如果響應(yīng)事件的控件調(diào)用了 super touchs等方法,那么事件會沿著響應(yīng)鏈向下傳遞,傳遞給下一個響應(yīng)者,這個響應(yīng)者來調(diào)用touchs系列方法;
- 如果父視圖不接收處理事件,那么他的子視圖也不能接收到;
- 事件傳遞是由父控件向子控件傳遞的,事件響應(yīng)是由子控件向父控件出啊低的;