iOS事件攔截和事件轉發
響應鏈流程
基本流程
大家都知道 iOS 的響應鏈是 UIApplication 收到使用者觸控螢幕的事件以後通過逐層尋找最後得到使用者觸控的 View 也就是第一響應者,然後呼叫 View 的 touchesBegan:withEvent:
方法處理事件任務的流程.大概流程是這樣的:
圖片很清晰的說明了查詢流程 AppDelegate 收到事件逐層查詢.最終找到 UIButton 這個響應者 然後呼叫 UIButton 的touchesBegan:withEvent:
方法處理事件.
如何查詢第一響應者
查詢第一響應者主要涉及以下兩個方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
pointInside:
通過 point 引數確定觸碰點是否在當前 View 的響應範圍內 是則返回YES 否則返回 NO 實現方法大概是這個樣子的
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.bounds, point);
}
hitTest方法:
- 它首先會通過呼叫自身的 pointInside 方法判斷使用者觸控的點是否在當前物件的響應範圍內,如果 pointInside 方法返回 NO hitTest方法直接返回 nil
- 如果 pointInside 方法返回 YES hitTest方法接著會判斷自身是否有子檢視.如果有則呼叫頂層子檢視的 hitTest 方法 直到有子檢視返回 View
- 如果所有子檢視都返回 nil hitTest 方法返回自身.
hitTest方法的內部實現虛擬碼
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 判斷觸控位置是否在當前檢視內
if ([self pointInside:point withEvent:event]) {
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個檢視開始查詢
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 轉換座標系 使座標基於子檢視
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子檢視 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子檢視返回一個view 就直接返回 不在繼續遍歷
if (view) {
return view;
}
}
// 所有子檢視都沒有返回 則返回自身
return self;
}
return nil;
}
事件傳遞
找到第一響應者 application 便會根據 event 呼叫第一響應者響應的
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);
第一響應者在這幾個方法中處理響應的事件,處理完成後根據需要呼叫 nextResponder 的 touch 方法,通常 nextResponder 就是第一響應者的 superView 文章的第一張圖倒著看就是nextResponder 的順序
事件攔截
通常第一響應者都是響應鏈中最末端的響應者,事件攔截就是在響應鏈中截獲事件,停止下發.將事件交由中間的某個響應者執行.比如這樣:
通常點選紅色 view 事件將交由 紅色 view 處理.如果想讓粉色 View 或者綠色 view 處理事件應該怎麼辦?
有兩種辦法
- 在紅色 view 的的 touch 方法中呼叫父類或者 nextResponder 的
touch 方法 - 在需要攔截的 view 中重寫 hitTest 方法改變第一響應者
首先來看第一種
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 將事件傳遞給下一響應者
[self.nextResponder touchesBegan:touches withEvent:event];
// 呼叫父類的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;
}
這種方法比較簡單粗暴.實現後 所有 subview 將不再能夠接受任何事件 具體使用那種方式看需求.當然還可以通過 event 或者 point 有針對性的攔截
事件轉發
有時候還需要將事件轉發出去.讓本來不能響應事件的 view 響應事件,最常用的場景就是讓子檢視超出父檢視的部分也能響應事件,比如要實現這樣的 tabbar
橙色按鈕有兩個區域 a 區超出父檢視 b 區沒有超出父檢視,如果不作處理,那麼點選 a 區是無法響應事件的,因為 a 區域的座標不在父檢視的範圍內,當執行到父檢視的 pointInside 的時候就會返回 NO
想要讓 a 區響應事件 就需要重寫父檢視的 pointInside 或 hitTest 方法讓 pointInside 返回 YES 或 讓hitTest 直接返回橙色檢視
重寫hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 觸控點在檢視範圍內 則交由父類處理
if ([self pointInside:point withEvent:event]) {
return [super hitTest:point withEvent:event];
}
// 如果觸控點不在範圍內 而在子檢視範圍內依舊返回子檢視
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個檢視開始查詢
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 轉換座標系 使座標基於子檢視
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子檢視 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子檢視返回一個view 就直接返回 不在繼續遍歷
if (view) {
return view;
}
}
return nil;
}
重寫 pointInside 方法原理相同 重點注意轉換座標系 就算他們不是一條響應鏈上 也可以通過重寫 hitTest 方法轉發事件.原理相同的東西就不再寫了
擴充套件
關於手勢的處理邏輯和這個相同.但是手勢的優先順序更高.如果父檢視有手勢.預設優先處理手勢事件 可以修改手勢的屬性cancelsTouchesInView
為 NO 來同時處理手勢和普通觸控事件
相關文章
- 2.攔截WebView事件WebView事件
- js 建立和觸發事件 和 自定義事件JS事件
- 記錄下:iOS事件的事件的傳遞和響應iOS事件
- iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求iOSProtocolHTTP
- Vue事件獲取觸發事件物件和繫結事件物件Vue事件物件
- onscroll 事件和onScrollCapture事件事件APT
- React 事件和 Dom 事件React事件
- touch事件和click事件多次觸發的問題事件
- Spring MVC 中的攔截器的使用“攔截器基本配置” 和 “攔截器高階配置”SpringMVC
- 事件和事件監聽器事件
- 事件模型和事件委託事件模型
- 事件分發和處理事件
- VUE-UNI事件轉發監聽Vue事件
- JS事件流和事件委託JS事件
- 事件系統-z 事件發現事件
- IOS繫結touchend事件失效iOS事件
- IIS 攔截主頁和SwaggerSwagger
- shiro攔截後地址跳轉跨域跨域
- 企圖為vuex新增發布訂閱:事件繫結和事件觸發Vue事件
- nodejs事件和事件迴圈簡介NodeJS事件
- nodejs事件和事件迴圈詳解NodeJS事件
- javascript–BOM的onload事件和onunload事件JavaScript事件
- Asp.Netcore使用Filter來實現介面的全域性異常攔截,以及前置攔截和後置攔截ASP.NETNetCoreFilter
- iOS與JS互動之UIWebView協議攔截iOSJSUIWebView協議
- 事件分發之View事件處理事件View
- Latch free等待事件(轉)事件
- Qt 事件傳遞流程-事件處理器|事件分發器|事件過濾器QT事件過濾器
- Proxy 攔截
- java springboot監聽事件和處理事件JavaSpring Boot事件
- JS學習之事件和事件繫結JS事件
- Javascript中的事件物件和事件型別JavaScript事件物件型別
- JS的事件繫結和事件流模型JS事件模型
- 關於js事件冒泡和事件捕獲JS事件
- JQuery4:滑鼠事件和滾動事件jQuery事件
- 理解js的事件冒泡和事件捕獲JS事件
- Spring 過濾器和攔截器Spring過濾器
- SpringMVC攔截器,設定不攔截的URLSpringMVC
- 淺談 iOS 事件的傳遞和響應過程iOS事件
- spring mvc攔截器,spring攔截器以及AOP切面的區別和原始碼SpringMVC原始碼