iOS事件響應鏈(Responder Chain)

  • 概述

在iOS中,視圖的層級通常都是 父視圖->添加各類子視圖。這時候某個視圖(子視圖)上有個按鈕,須要咱們交互。可是有時候咱們會發現不管如何都沒有反應。這時候可能就是咱們對iOS的事件傳遞響應還有些迷茫。數組

  1. 事件的傳遞:簡單的來講就是事件的傳遞順序。他是系統向可響應的離用戶最近的視圖傳遞。大體流程就是 UIKit -> ... -> root view -> ... -> initial view 。(方式是從上到下傳遞)
  2. 事件的響應:在咱們的視圖中通常都是樹狀結構,有層級關係。那麼這時候用戶點擊某個控件,所觸發的是子視圖仍是父視圖,這種有一個前後的關係,就構成了一個鏈條,咱們就叫作「響應者鏈條」。響應的大體順序就是,首先查看initial view 是否可以處理這個事件,若是不能事件上傳給其父視圖;如若上級視圖仍然不可以處理則會繼續上傳;一直傳遞到視圖的控制器,那麼首先判斷該控制器的根視圖View是否可以處理此事件;若是不能那麼繼續上傳(對於目前自己的視圖控制器自己還在另外一個視圖控制器中,則繼續交由給其父控制器的根視圖繼續處理,若是不能那麼就要交給父控制器的控制器來處理);一直到 window,若是仍是不能處理,那麼就要交給 application 處理,仍是不能那麼就被丟棄。(傳遞方式是從下到上傳遞)
  • iOS中的事件
  1. 觸摸事件
  2. 加速計事件
  3. 遠程控制事件
  • 觸摸事件

  響應者對象(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;
}
相關文章
相關標籤/搜索