在iOS中,視圖的層級通常都是 父視圖->添加各類子視圖。這時候某個視圖(子視圖)上有個按鈕,須要咱們交互。可是有時候咱們會發現不管如何都沒有反應。這時候可能就是咱們對iOS的事件傳遞響應還有些迷茫。數組
響應者對象(UIResponder)app
在iOS中,只要是繼承UIResponder的對象均可以接收並處理事件。在iOS中提供了一些方法來處理觸摸事件。ide
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 開始觸摸View時會調用一次 - (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; // 觸摸結束前,電話打進來,會自動調用這個方法
事件的產生spa
當發生一個觸摸事件後,系統會將觸摸事件添加到UIApplication管理的事件隊列中(先進先出) -> UIApplication 從事件隊列中拿出最前的事件將之分發出去,一般是首先發送事件給應用程序的主窗口 -> 主窗口會找到一個最合適的視圖來處理觸摸事件 -> 找到合適的視圖控件後,就會調用控件的上述方法中的一個或者多個來處理具體的事件處理。code
事件的傳遞對象
主窗口先判斷能不能接收這個觸摸事件,如若不能,就直接return;blog
主窗口能夠接收,傳遞給子視圖,繼續判斷,繼續傳遞,循環直到沒有可以符合響應的子控件,那麼這時候的就會認爲由本身來處理這個事件最合適。繼承
也有不能響應的狀況:隊列
1. 不容許交互事件
2. 控件隱藏
3. 透明度太低(<0.01)
如何尋找最適合的控件來處理事件
UIView 及其子類有兩個很是重要的方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
當只要有事件傳遞給這個控件,這個控件就會調用
hitTest: withEvent:
其做用是尋找並返回最合適的View,無論這個控件能不能處理事件,也無論觸摸點是否是在這個空間上,都會先接收事件,而後調用方法。
因此這裏咱們就有了可操做空間 , 由於無論點擊事件發生在哪裏,最終可以處理事件的View都是這個方法返回的View。經過重寫這個方法咱們能夠攔截整個事件的傳遞過程,同時能夠指定處理事件的View。(若是這個方法返回的是nil,那麼調用該方法的控件自己以及其子控件均不能處理事件,只能由其父視圖來處理事件)
因此事件的傳遞順序 :產生觸摸事件 -> UIApplication事件隊列 -> [UIWindow hitTest:withEvent:] -> 返回更合適的View -> [子控件 hitTest:withEvent:] -> 返回最合適的View ...
因此這裏咱們能夠獲得的結論就是:無論子控件是否是最合適的View,都會調用 hitTest 方法,若是不是最合適的View,會返回nil,同時認定其父視圖是最合適的View。
小技巧:在父控件中返回最合適的子控件。由於若是在本身返回本身,有可能兩個視圖 B,C 同時加載 A 上,當設置B爲最合適的View,這時候若是咱們在 B 中返回本身,可能咱們點擊到 C 這時候 B 還沒來及返回系統就已經定位到了 C 。
尋找最合適的View底層剖析
// 何時調用:只要事件一傳遞給一個控件,那麼這個控件就會調用本身的這個方法 // 做用:尋找並返回最合適的view // UIApplication -> [UIWindow hitTest:withEvent:]尋找最合適的view告訴系統 // point:當前手指觸摸的點 // point:是方法調用者座標系上的點 - (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.從後往前遍歷子控件數組 int count = (int)self.subviews.count; for (int i = count - 1; i >= 0; i--) { // 獲取子控件 UIView *childView = self.subviews[i]; // 座標系的轉換,把窗口上的點轉換爲子控件上的點 // 把本身控件上的點轉換成子控件上的點 CGPoint childP = [self convertPoint:point toView:childView]; UIView *fitView = [childView hitTest:childP withEvent:event]; if (fitView) { // 若是能找到最合適的view return fitView; } } // 4.沒有找到更合適的view,也就是沒有比本身更合適的view return self; }
經過重寫 View 的 hitTest 方法,便可找到最合適的 View
另外一個比較重要的方法
pointInside: withEvent:
方法是用來判斷咱們觸摸事件的點位置是否在當前View上,若是返回 NO 說明是不在當前 View 座標系上,同時天然是不可以處理事件的。
事件的響應
傳遞方式是 從下往上 的傳遞方式。
事件處理流程
產生觸摸事件 -> 事件添加到 UIApplication 隊列中 -> 事件傳遞主窗口 -> 找到最合適的View -> 最合適的View調用本身的touch方法來處理事件 -> touches默認作法是把事件順着響應鏈往上傳遞
//只要點擊控件,就會調用touchBegin,若是沒有重寫這個方法,本身處理不了觸摸事件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 默認會把事件傳遞給上一個響應者,上一個響應者是父控件,交給父控件處理 [super touchesBegan:touches withEvent:event]; // 注意不是調用父控件的touches方法,而是調用父類的touches方法 // super是父類 superview是父控件 }
當咱們須要作到一個事件多個對象同時處理的話,咱們就能夠先處理本身的事件以後,調用 super 方法。
當咱們要擴大按鈕點擊範圍
好比咱們有一個 20pt*20pt 的 按鈕,咱們能夠在一個控件的中利用 hitTest 來實現。 例如一個 UIButton,自定義一個按鈕,在其自定義類中重寫方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // 1.判斷下窗口可否接收事件 if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil; // 擴大到按鈕以外的都是點擊範圍 CGRect touchRect = CGRectInset(self.bounds, -20, -20); if (CGRectContainsPoint(touchRect, point)) { for (UIView *subView in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subView convertPoint:point toView:self]; UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; }
將事件傳遞給兄弟View(A 與 B是同一個父視圖,可是 B 有部分遮擋住了 A ;點擊遮擋部分須要 A 響應事件)這時候點擊 A 是不會有任何響應的,除非 B 的userInteractionEnable 爲 NO , 可是咱們用 hitTest 一樣能夠作到,重寫 B 的這個方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitTestView = [super hitTest:point withEvent:event]; if (hitTestView == self) { hitTestView = nil; } return hitTestView; }