本文章將記錄有關(guān)iOS事件的傳遞機(jī)制,如有錯(cuò)誤歡迎指出~
iOS的事件分為3大類型
Touch Events(觸摸事件)
Motion Events(運(yùn)動(dòng)事件夏伊,比如重力感應(yīng)和搖一搖等)
Remote Events(遠(yuǎn)程事件摇展,比如用耳機(jī)上得按鍵來控制手機(jī))
在開發(fā)中,最常用到的就是Touch Events(觸摸事件)溺忧,基本貫穿于每個(gè)App中咏连,也是本文的豬腳~ 因此文中所說事件均特指觸摸事件。
接下來鲁森,記錄祟滴、涉及的問題大致包括:
事件是怎么找它的媽媽的?(尋找事件的最佳響應(yīng)者)
事件又是如何去到媽媽的身邊的歌溉?媽媽又將如何對待它垄懂?(事件的響應(yīng)及在響應(yīng)鏈中的傳遞)
尋找事件的最佳響應(yīng)者(Hit-Testing)
當(dāng)我們觸摸屏幕的某個(gè)可響應(yīng)的功能點(diǎn)后,最終都會由UIView或者繼承UIView的控件來響應(yīng)
那我們先來看下UIView的兩個(gè)方法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回尋找到的最終響應(yīng)這個(gè)事件的視圖
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
?
// default returns YES if point is in bounds
//判斷某一個(gè)點(diǎn)擊的位置是否在視圖范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
每個(gè)UIView對象都有一個(gè) hitTest: withEvent:
方法痛垛,這個(gè)方法是Hit-Testing
過程中最核心的存在草慧,其作用是詢問事件在當(dāng)前視圖中的響應(yīng)者,同時(shí)又是作為事件傳遞的橋梁匙头。
看看它是什么時(shí)候被調(diào)用的
當(dāng)手指接觸屏幕漫谷,UIApplication接收到手指的觸摸事件之后,就會去調(diào)用UIWindow的
hitTest: withEvent:
方法在
hitTest: withEvent:
方法中會調(diào)用pointInside: withEvent:
去判斷當(dāng)前點(diǎn)擊的point是否屬于UIWindow范圍內(nèi)蹂析,如果是舔示,就會以倒序的方式遍歷它的子視圖碟婆,即越后添加的視圖,越先遍歷子視圖也調(diào)用自身的
hitTest: withEvent:
方法惕稻,來查找最終響應(yīng)的視圖
再來看個(gè)示例:
視圖層級如下(同一層級的視圖越在下面竖共,表示越后添加):
A
├── B
│ └── D
└── C
├── E
└── F
現(xiàn)在假設(shè)在E視圖所處的屏幕位置觸發(fā)一個(gè)觸摸,App接收到這個(gè)觸摸事件事件后缩宜,先將事件傳遞給UIWindow肘迎,然后自下而上開始在子視圖中尋找最佳響應(yīng)者。事件傳遞的順序如下所示:
UIWindow將事件傳遞給其子視圖A
A判斷自身能響應(yīng)該事件锻煌,繼續(xù)將事件傳遞給C(因?yàn)橐晥DC比視圖B后添加妓布,因此優(yōu)先傳給C)。
C判斷自身能響應(yīng)事件宋梧,繼續(xù)將事件傳遞給F(同理F比E后添加)匣沼。
F判斷自身不能響應(yīng)事件,C又將事件傳遞給E捂龄。
E判斷自身能響應(yīng)事件释涛,同時(shí)E已經(jīng)沒有子視圖,因此最終E就是最佳響應(yīng)者倦沧。
以上唇撬,就是尋找最佳響應(yīng)者的整個(gè)過程。
接下來展融,來看下hitTest: withEvent:
方法里窖认,都做些了什么?
我們已經(jīng)知道事件在響應(yīng)者之間的傳遞告希,是視圖通過判斷自身能否響應(yīng)事件來決定是否繼續(xù)向子視圖傳遞扑浸,那么判斷響應(yīng)的條件是什么呢?
視圖響應(yīng)事件的條件:
允許交互:
userInteractionEnabled = YES
禁止隱藏:
hidden = NO
透明度:
alpha > 0.01
觸摸點(diǎn)的位置:通過
pointInside: withEvent:
方法判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)
代碼的表現(xiàn)大概如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3種狀態(tài)無法響應(yīng)事件
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
//觸摸點(diǎn)若不在當(dāng)前視圖上則無法響應(yīng)事件
if ([self pointInside:point withEvent:event]) {
//從后往前遍歷子視圖數(shù)組
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
// 坐標(biāo)系的轉(zhuǎn)換,把觸摸點(diǎn)在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
//詢問子視圖層級中的最佳響應(yīng)視圖
UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//如果子視圖中有更合適的就返回
return hitTestView;
}
}
//沒有在子視圖中找到更合適的響應(yīng)視圖燕偶,那么自身就是最合適的
return self;
}
return nil;
}
說了這么多喝噪,那我們可以運(yùn)用hitTest: withEvent:
來搞些什么事情呢
使超出父視圖坐標(biāo)范圍的子視圖也能響應(yīng)事件
視圖層級如下:
A
├── B
如上圖所示,視圖B有一部分是不在父視圖A的坐標(biāo)范圍內(nèi)的指么,當(dāng)我們觸摸視圖B的上半部分酝惧,是不會響應(yīng)事件的。當(dāng)然伯诬,我們可以通過重寫視圖A的 hitTest: withEvent:
方法來解決這個(gè)需求晚唇。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
//如果找不到合適的響應(yīng)者
if (view == nil) {
//視圖B坐標(biāo)系的轉(zhuǎn)換
CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
// 滿足條件,返回視圖B
view = self.deleteButton;
}
}
return view;
}
在視圖A的hitTest: withEvent:
方法中判斷觸摸點(diǎn),是否位于視圖B的視圖范圍內(nèi)姑廉,如果屬于缺亮,則返回視圖B翁涤。這樣一來桥言,當(dāng)我們點(diǎn)擊視圖B的任何位置都可以響應(yīng)事件了萌踱。
注:文章底部有簡單的Demo(僅供參考)
事件的響應(yīng)及在響應(yīng)鏈中的傳遞
經(jīng)歷Hit-Testing后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了号阿,接下來要做的事情就是:
將事件傳遞給最佳響應(yīng)者響應(yīng)
事件沿著響應(yīng)鏈傳遞
事件傳遞給最佳響應(yīng)者
最佳響應(yīng)者具有最高的事件響應(yīng)優(yōu)先級并鸵,因此UIApplication會先將事件傳遞給它供其響應(yīng)。
UIApplication中有個(gè)sendEvent:
的方法扔涧,在UIWindow中同樣也可以發(fā)現(xiàn)一個(gè)同樣的方法园担。UIApplication是通過這個(gè)方法把事件發(fā)送給UIWindow,然后UIWindow通過同樣的接口枯夜,把事件發(fā)送給最佳響應(yīng)者弯汰。
以尋找事件的最佳響應(yīng)者一節(jié)中點(diǎn)擊視圖E為例,在EView的 touchesBegan:withEvent:
上打個(gè)斷點(diǎn)查看調(diào)用棧就能看清這一過程:
當(dāng)事件傳遞給最佳響應(yīng)者后湖雹,響應(yīng)者響應(yīng)這個(gè)事件咏闪,則這個(gè)事件到此就結(jié)束了,它會被釋放摔吏。假設(shè)響應(yīng)者沒有響應(yīng)這個(gè)事件鸽嫂,那么它將何去何從?事件將會沿著響應(yīng)鏈自上而下傳遞征讲。
注意:
尋找最佳響應(yīng)者一節(jié)中也說到了事件的傳遞据某,與此處所說的事件的傳遞有本質(zhì)區(qū)別。上面所說的事件傳遞的目的是為了尋找事件的最佳響應(yīng)者诗箍,是自下而上(父視圖到子視圖)的傳遞癣籽;而這里的事件傳遞目的是響應(yīng)者做出對事件的響應(yīng),這個(gè)過程是自上而下(子視圖到父視圖)的扳还。前者為“尋找”才避,后者為“響應(yīng)”。
事件沿著響應(yīng)鏈傳遞
在UIKit中有一個(gè)類:UIResponder氨距,它是所有可以響應(yīng)事件的類的基類桑逝。來看下它的頭文件的幾個(gè)屬性和方法
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
--------------省略部分代碼------------
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
UIApplication,UIViewController和UIView都是繼承自它俏让,都有一個(gè) nextResponder
方法楞遏,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個(gè)響應(yīng)者,也通過nextResponder
來串成響應(yīng)鏈首昔。
在App中寡喝,所有的視圖都是根據(jù)樹狀層次結(jié)構(gòu)組織起來的,因此勒奇,每個(gè)View都有自己的SuperView预鬓。當(dāng)一個(gè)View被add到SuperView上的時(shí)候,它的nextResponder
屬性就會被指向它的SuperView赊颠,各個(gè)不同響應(yīng)者的指向如下:
UIView 若視圖是控制器的根視圖格二,則其
nextResponder
為控制器對象劈彪;否則,其nextResponder
為父視圖顶猜。UIViewController 若控制器的視圖是window的根視圖沧奴,則其
nextResponder
為窗口對象;若控制器是從別的控制器present出來的长窄,則其nextResponder
為presenting view controller滔吠。UIWindow
nextResponder
為UIApplication對象。UIApplication 若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對象挠日,且不是UIView疮绷、UIViewController或app本身,則UIApplication的
nextResponder
為app delegate嚣潜。
這樣矗愧,整個(gè)App就通過nextResponder
串成了一條鏈,也就是我們所說的響應(yīng)鏈郑原,子視圖指向父視圖構(gòu)成的響應(yīng)鏈唉韭。
看一下官網(wǎng)對于響應(yīng)鏈的示例展示
若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的犯犁,則其nextResponder
為UIViewController對象属愤;若是直接add在UIWindow上的,則其nextResponder
為UIWindow對象酸役。
響應(yīng)者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent:
方法控制的住诸,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞。
響應(yīng)者對于接收到的事件有3種操作:
不攔截涣澡,默認(rèn)操作 事件會自動(dòng)沿著默認(rèn)的響應(yīng)鏈往下傳遞
攔截贱呐,不再往下分發(fā)事件 重寫
touchesBegan:withEvent:
進(jìn)行事件處理,不調(diào)用父類的touchesBegan:withEvent:
攔截入桂,繼續(xù)往下分發(fā)事件 重寫
touchesBegan:withEvent:
進(jìn)行事件處理奄薇,同時(shí)調(diào)用父類的touchesBegan:withEvent:
將事件往下傳遞
因此,你也可以通過 touchesBegan:withEvent:
方法搞點(diǎn)事情~
總結(jié)
觸摸事件先通過自下而上(父視圖-->子視圖)的傳遞方式尋找最佳響應(yīng)者抗愁,
然后以自上而下(子視圖-->父視圖)的方式在響應(yīng)鏈中傳遞馁蒂。
Github :TouchEventDemo(僅供參考)