iOS 探討之 事件與響應者

題外話
     蘋果忙着發佈新產品,我則忙着處理那些討厭的數據...爲了堅持最初的信念,在10月最後一天把自己以前總結的一篇文章拿出來給大家分享一下吧。


概述
用戶與設備交互的話題再怎麼探討也不爲過,這幾天尋了幾篇文章,寫的都很不錯,大致梳理一下,留個念想。

在iOS中,用戶與設備的交互事件可以分爲3類:
+ 觸控事件
+ 傳感器事件
+ 遠程控制事件

本次將主要探討觸控事件中的事件傳遞、響應者、響應者鏈條。

圖 1-1 用戶與設備交互方式


探討
1.  基礎信息
     響應者 (Responsder Object) : 能夠響應並且處理事件的對象,且繼承UIResponder的對象稱之爲響應者對象,能夠處理touchesBegan等觸摸事件。
     第一響應者 (First Responder) : 第一個接收事件的View對象。
     響應者鏈條: 有很多響應者鏈接在一起組合起來的一個鏈條稱之爲響應者鏈條。

2.  事件傳遞
     鄙人愚見,事件傳遞可以認爲是App尋找最佳執行此事件響應者的過程。

圖 1-2 用戶與設備交互事件傳遞


     用戶與 iOS設備產生交互後,當前App的UIApplication管理事件隊列會加入此事件來等待處理。待App準備處理此事件時,會從準備隊列中移除這個事件,App會尋找最佳執行此事件的Target,故會將事件傳遞給KeyWindow(UIWindow),如果此時該ViewController沒有加入GestureRecognizer 那麼緊接着會傳遞給View(視圖層次(父->子)),以尋找最佳執行Target。


圖 1-3 觸摸事件實例圖


觸摸事件舉例:
點擊橘色的View: UIApplication事件隊列 -> UIWindow -> 藍色 -> 橘色
點擊綠色的View: UIApplication事件隊列 -> UIWindow -> 藍色 -> 白色 -> 綠色
點擊紅色的View: UIApplication事件隊列 -> UIWindow -> 藍色 -> 白色 -> 紅色

傳遞詳解:
KeyWindow會在它的內容視圖上調用 hitTest:withEvent: (該方法返回的就是處理此觸摸事件的最合適view)來完成這個找尋過程。
hitTest:withEvent: 在內部首先會判斷該視圖是否能響應觸摸事件,如果不能響應,返回nil,表示該視圖不響應此觸摸事件。
然後再調用pintInside:withEvent:(該方法用來判斷點擊事件發生的位置是否處於當前視圖範圍內)。
如果pointInside:withEvent:返回NO,那麼hitTest:withEvent:也直接返回nil。
如果pointInside:withEvent:返回YES,則向當前視圖的所有子視圖發送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到最底層視圖,即從subviews數組的末尾向前遍歷。直到有子視圖返回非空對象或者全部子視圖遍歷完成;若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象,處理結束;若所有子視圖都返回nil,則hitTest:withEvent:方法返回該視圖自身。

3. 探討 hitTest:withEvent: 方法的底層實現
不接收觸摸事件的三種情況
+ 不接收用戶交互 userInterationEnabled = NO
+ 隱藏 hidden = YES
+ 透明 alpha = 0.0~0.01

底層實現
// point是該視圖的座標系上的點
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判斷自己能否接收觸摸事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01return nil;
    // 2.判斷觸摸點在不在自己範圍內
    if (![self pointInside:point withEvent:event]) return nil;
   // 3.從後往前遍歷自己的子控件,看是否有子控件更適合響應此事件
    NSInteger count = self.subviews.count;
   for (NSIntegeri = count - 1; i >= 0; i--) {
       UIView *childView = self.subviews[i];
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 沒有找到比自己更合適的view
    return self;
}

4. 響應者鏈條
     每個能執行hitTest:withEvent:方法的View都屬於事件傳遞的一部分,但是隻有pointInside:withEvent:返回爲YES的View才屬於響應者鏈條。

處理原則
     響應者鏈條其實還包括 視圖控制器(ViewController)、UIWindow、UIApplication。如下圖:



圖 1-4 響應鏈


     通過事件傳遞找到最合適的處理觸摸事件的View後(就是最後一個pintInside:withEvent:返回YES的View,它是第一響應者),如果該view是控制器view,那麼上一個響應者就是控制器。如果它不是控制器View,那麼上一個響應者就是前面一個pointInside:withEvent:返回YES的view(父控件)。最後這些所有pointInside:withEvent:返回YES的view加上它們的控制器、UIWindow、UIApplication共同構成響應者鏈條。
     響應者鏈條是自上而下的(頂層->UIApplication),前面的事件傳遞是自下而上的(UIApplication->頂層)。

5. 響應者鏈條應用
可以讓一個觸摸事件讓多個響應者同時處理該事件。如在上圖中多個View中打印touchBegan:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchBegan---%@", [self class]);
    [super touchesBegan:touches withEvent:event];
}


總結
1. 知識擴展
     如果將某個view的pointInside:withEvent:方法直接返回NO(無論子控件的pointInside:withEvent:返回什麼結果),影響的是子控件區域和自身區域的點擊事件處理,這些區域不再響應事件。其餘區域響應點擊事件不發生變化。
     如果將某個view的pointInside:withEvent:方法直接返回YES,自身區域響應點擊事件不變。其它改變:
     首先,父控件所有區域點擊事件交給該view處理。
     然後,再看該view處於父控件的子控件數組中的位置。數組前面的兄弟控件的點擊事件交給該view處理,數組後面的兄弟控件的點擊事件由其兄弟控件處理。
     最後,該view的子控件原來能夠自己處理點擊的區域繼續由子控件處理,子控件原先不能夠自己處理點擊的(超出view範圍)區域可以由子控件處理了。
     所以,想要屏蔽掉某個view響應點擊事件,如果其沒有子控件或者子控件響應事件也想屏蔽掉,直接將該View的pointInside:withEvent:返回爲NO就行了。而在一般情況下,不建議將view的pointInside:withEvent:返回YES.

2. 什麼時候重寫hitTest:withEvent:,什麼時候重寫pointInside:withEvent:,在哪個view內重寫它們?
     很多情況下hitTest:withEvent:和pointInside:withEvent:方法任選其一都可以實現某個功能,比如在屏蔽中,pointInside:withEvent:返回NO就可以實現的話,都可以用hitTest:withEvent:返回nil代替。
     但是,hitTest:withEvent:更強大。一般pointInside:withEvent:在一般情況下起內部頂多只能根據情況判斷怎麼返回NO,屏蔽掉自己和子控件的事件響應。所以只要是想要保留子控件對觸摸事件響應,屏蔽其父控件的響應,單獨重寫pointInside:withEvent:無法辦到,必須重寫hitTest:withEvent:方法。
     觸摸事件原來該由某個view響應,現在你不想讓它處理而讓別的控件處理,那麼就應該在該view內重寫hitTest:withEvent:或pointInside:withEvent:方法。


參考文獻
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章