iOS事件響應鏈(Responder Chain),iosresponder
在iOS中,視圖的層級一般都是 父視圖->添加各種子視圖。這時候某個視圖(子視圖)上有個按鈕,需要我們互動。但是有時候我們會發現無論如何都沒有反應。這時候可能就是我們對iOS的事件傳遞響應還有些迷茫。
響應者對象(UIResponder)
在iOS中,只要是繼承UIResponder的對象都可以接收並處理事件。在iOS中提供了一些方法來處理觸摸事件。
- (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; // 觸摸結束前,電話打進來,會自動調用這個方法
事件的產生
當發生一個觸摸事件後,系統會將觸摸事件添加到UIApplication管理的事件隊列中(先進先出) -> UIApplication 從事件隊列中拿出最前的事件將之分發出去,通常是首先發送事件給應用程式的主視窗 -> 主視窗會找到一個最合適的視圖來處理觸摸事件 -> 找到合適的視圖控制項後,就會調用控制項的上述方法中的一個或者多個來處理具體的事件處理。
事件的傳遞
主視窗先判斷能不能接收這個觸摸事件,如若不能,就直接return;
主視窗可以接收,傳遞給子視圖,繼續判斷,繼續傳遞,迴圈直到沒有能夠符合響應的子控制項,那麼這時候的就會認為由自己來處理這個事件最合適。
也有不能響應的情況:
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;}