響應鏈流程
基本流程
大家都知道 iOS 的響應鏈是 UIApplication 收到用戶觸摸屏幕的事件以后通過逐層尋找最后得到用戶觸摸的 View 也就是第一響應者,然后調(diào)用 View 的 touchesBegan:withEvent:
方法處理事件任務的流程.大概流程是這樣的:
圖片很清晰的說明了查找流程 AppDelegate 收到事件逐層查找.最終找到 UIButton 這個響應者 然后調(diào)用 UIButton 的touchesBegan:withEvent:
方法處理事件.
如何查找第一響應者
查找第一響應者主要涉及以下兩個方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
pointInside:
通過 point 參數(shù)確定觸碰點是否在當前 View 的響應范圍內(nèi) 是則返回YES 否則返回 NO 實現(xiàn)方法大概是這個樣子的
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.bounds, point);
}
hitTest方法:
- 它首先會通過調(diào)用自身的 pointInside 方法判斷用戶觸摸的點是否在當前對象的響應范圍內(nèi),如果 pointInside 方法返回 NO hitTest方法直接返回 nil
- 如果 pointInside 方法返回 YES hitTest方法接著會判斷自身是否有子視圖.如果有則調(diào)用頂層子視圖的 hitTest 方法 直到有子視圖返回 View
- 如果所有子視圖都返回 nil hitTest 方法返回自身.
hitTest方法的內(nèi)部實現(xiàn)偽代碼
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 判斷觸摸位置是否在當前視圖內(nèi)
if ([self pointInside:point withEvent:event]) {
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個視圖開始查找
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 轉(zhuǎn)換坐標系 使坐標基于子視圖
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子視圖 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子視圖返回一個view 就直接返回 不在繼續(xù)遍歷
if (view) {
return view;
}
}
// 所有子視圖都沒有返回 則返回自身
return self;
}
return nil;
}
事件傳遞
找到第一響應者 application 便會根據(jù) event 調(diào)用第一響應者響應的
touch 方法:
- (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);
第一響應者在這幾個方法中處理響應的事件,處理完成后根據(jù)需要調(diào)用 nextResponder 的 touch 方法,通常 nextResponder 就是第一響應者的 superView 文章的第一張圖倒著看就是nextResponder 的順序
事件攔截
通常第一響應者都是響應鏈中最末端的響應者,事件攔截就是在響應鏈中截獲事件,停止下發(fā).將事件交由中間的某個響應者執(zhí)行.比如這樣:
通常點擊紅色 view 事件將交由 紅色 view 處理.如果想讓粉色 View 或者綠色 view 處理事件應該怎么辦?
有兩種辦法
- 在紅色 view 的的 touch 方法中調(diào)用父類或者 nextResponder 的
touch 方法 - 在需要攔截的 view 中重寫 hitTest 方法改變第一響應者
首先來看第一種
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 將事件傳遞給下一響應者
[self.nextResponder touchesBegan:touches withEvent:event];
// 調(diào)用父類的touch方法 和上面的方法效果一樣 這兩句只需要其中一句
[super touchesBegan:touches withEvent:event];
}
這種方法有兩個問題,你需要重寫所有的 touch 方法并且還要重寫要攔截事件的 view 與頂級 view 之間的所有 view 的 touch 方法
第二種方法
重寫攔截事件的 view 的 hitTest 方法 比如要讓綠色的 view 處理事件 就重寫綠色 view 的 hitTest 方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 如果在當前 view 中 直接返回 self 這樣自身就成為了第一響應者 subViews 不再能夠接受到響應事件
if ([self pointInside:point withEvent:event]) {
return self;
}
return nil;
}
這種方法比較簡單粗暴.實現(xiàn)后 所有 subview 將不再能夠接受任何事件 具體使用那種方式看需求.當然還可以通過 event 或者 point 有針對性的攔截
事件轉(zhuǎn)發(fā)
有時候還需要將事件轉(zhuǎn)發(fā)出去.讓本來不能響應事件的 view 響應事件,最常用的場景就是讓子視圖超出父視圖的部分也能響應事件,比如要實現(xiàn)這樣的 tabbar
橙色按鈕有兩個區(qū)域 a 區(qū)超出父視圖 b 區(qū)沒有超出父視圖,如果不作處理,那么點擊 a 區(qū)是無法響應事件的,因為 a 區(qū)域的坐標不在父視圖的范圍內(nèi),當執(zhí)行到父視圖的 pointInside 的時候就會返回 NO
想要讓 a 區(qū)響應事件 就需要重寫父視圖的 pointInside 或 hitTest 方法讓 pointInside 返回 YES 或 讓hitTest 直接返回橙色視圖
重寫hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 觸摸點在視圖范圍內(nèi) 則交由父類處理
if ([self pointInside:point withEvent:event]) {
return [super hitTest:point withEvent:event];
}
// 如果觸摸點不在范圍內(nèi) 而在子視圖范圍內(nèi)依舊返回子視圖
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個視圖開始查找
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 轉(zhuǎn)換坐標系 使坐標基于子視圖
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子視圖 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子視圖返回一個view 就直接返回 不在繼續(xù)遍歷
if (view) {
return view;
}
}
return nil;
}
重寫 pointInside 方法原理相同 重點注意轉(zhuǎn)換坐標系 就算他們不是一條響應鏈上 也可以通過重寫 hitTest 方法轉(zhuǎn)發(fā)事件.原理相同的東西就不再寫了
擴展
關于手勢的處理邏輯和這個相同.但是手勢的優(yōu)先級更高.如果父視圖有手勢.默認優(yōu)先處理手勢事件 可以修改手勢的屬性cancelsTouchesInView
為 NO 來同時處理手勢和普通觸摸事件