一篇搞定事件傳遞、響應者鏈條、hitTest和pointInside的使用

一篇搞定事件傳遞、響應者鏈條、hitTest和pointInside的使用

 

發生觸摸事件後,系統會將該事件加入到一個由UIApplication管理的事件隊列中,UIApplication會從事件隊列中取出最前面的事件,並將事件分發下去以便處理。一般,會先發送事件給應用程序的keyWindow,主窗口會在其視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這個找尋的過程就是事件傳遞。git

1、事件傳遞

傳遞過程示例

1.pnggithub


觸摸事件的傳遞是從父控件傳遞到子控件
點擊了綠色的view:UIApplication -> UIWindow -> 白色 -> 綠色
點擊了藍色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色
點擊了紅色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 紅色數組

傳遞過程詳解:

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

2、hitTest:withEvent方法的底層實現

不接收觸摸事件的三種狀況

(1)不接收用戶交互 userInteractionEnabled = NO
(2)隱藏 hidden = YES
(3)透明 alpha = 0.0 ~ 0.01ui

hitTest:底層實現

// 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]) return nil;
    // 3.從後往前遍歷本身的子控件,看是否有子控件更適合響應此事件
    int count = self.subviews.count;
    for (int i = 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;
}

只有弄清楚了hitTest:方法的底層實現,才能更容易理解事件傳遞機制。spa

3、一個示例

2.pngcode

如圖所示,視圖結構爲紅綠黃均爲黑的子控件,而白爲黃的子控件,如下是點擊不一樣顏色區域的打印結果:對象

1.點擊白色中間區域
2016-05-27 17:58:45.502 hitTest----BlackView (pointInside返回YES)
2016-05-27 17:58:45.502 hitTest----YellowView (pointInside返回YES)
2016-05-27 17:58:45.503 hitTest----WhiteView (pointInside返回YES)
2016-05-27 17:58:45.506 touchBegan---WhiteView
2016-05-27 17:58:45.506 touchBegan---YellowView
2016-05-27 17:58:45.506 touchBegan---BlackView
分析(先只看hitTest方法的打印結果,忽略touchesBegan):
首先是blackView的hitTest方法被調用,內部調用pointInside方法,返回YES,表示觸摸點在blackView範圍內。而後倒敘遍歷blackView的子控件數組,發送hitTest消息。若是子控件調用hitTest方法返回不爲空,就中斷遍歷。因此,最後一個子控件yellowView調用hitTest方法,在pointInside方法裏面發現觸摸點在本身範圍內,繼續向yellowView的子控件數組發送消息,此時whiteView在hitTest方法中將本身逐層返回出去。最後響應事件的就是whiteView。繼承

2.點擊超出黃色區域的白色區域
2016-05-27 18:00:38.372 hitTest----BlackView (pointInside返回YES)
2016-05-27 18:00:38.372 hitTest----YellowView (pointInside返回NO)
2016-05-27 18:00:38.372 hitTest----GreenView (pointInside返回YES)
2016-05-27 18:00:38.374 touchBegan---GreenView
2016-05-27 18:00:38.374 touchBegan---BlackView
分析:
前面相同,倒敘遍歷blackView子控件數組,最後一個子控件YellowView在pointInside方法裏面返回的是NO,從而其hitTest返回nil。接着遍歷到GreenView,能夠響應該觸摸事件,最後返回GreenView。
可得出結論,不屬於父控件範圍的子控件部分,子控件沒法響應該部分的觸摸事件。隊列

3.點擊黃色區域
2016-05-27 18:01:22.933 hitTest----BlackView (pointInside返回YES)
2016-05-27 18:01:22.933 hitTest----YellowView (pointInside返回YES)
2016-05-27 18:01:22.933 hitTest----WhiteView (pointInside返回NO)
2016-05-27 18:01:22.935 touchBegan---YellowView
2016-05-27 18:01:22.935 touchBegan---BlackView
分析:
遍歷到YellowView,pointInside返回YES。繼續遍歷YellowView的子控件,whiteView的pointInside返回NO,從而其hitTest返回nil。 因此YellowView在hitTest方法中將本身返回出去。

4.點擊綠色區域
2016-05-27 18:02:13.333 hitTest----BlackView (pointInside返回YES)
2016-05-27 18:02:13.334 hitTest----YellowView (pointInside返回NO)
2016-05-27 18:02:13.334 hitTest----GreenView (pointInside返回YES)
2016-05-27 18:02:13.335 touchBegan---GreenView
2016-05-27 18:02:13.335 touchBegan---BlackView

5.點擊紅色區域
2016-05-27 18:03:02.687 hitTest----BlackView (pointInside返回YES)
2016-05-27 18:03:02.687 hitTest----YellowView (pointInside返回NO)
2016-05-27 18:03:02.687 hitTest----GreenView (pointInside返回NO)
2016-05-27 18:03:02.687 hitTest----RedView (pointInside返回YES)
2016-05-27 18:03:02.689 touchBegan---RedView
2016-05-27 18:03:02.689 touchBegan---BlackView

6.點擊灰色區域
2016-05-27 18:04:08.176 hitTest----BlackView (pointInside返回YES)
2016-05-27 18:04:08.177 hitTest----YellowView (pointInside返回NO)
2016-05-27 18:04:08.177 hitTest----GreenView (pointInside返回NO)
2016-05-27 18:04:08.177 hitTest----RedView (pointInside返回NO)
2016-05-27 18:04:08.179 touchBegan---BlackView

括號內容非打印內容。在hitTest:方法的消息分發過程當中,並非全部包含觸摸點範圍的view都會經歷事件傳遞。以2爲例,yellowView的pointInside方法直接返回NO,那麼觸摸事件就不會傳遞到yellowView的子控件whiteView了。

4、響應者鏈條

分析到這裏,就能夠引出響應者鏈條這一律唸了。每一個能執行hitTest:方法的view都屬於事件傳遞的一部分,可是,只有pointInside返回YES的view才屬於響應者鏈條。與上述打印中的touchesBegan方法的打印結果一致。

相關概念

響應者:繼承UIResponder的對象稱之爲響應者對象,可以處理touchesBegan等觸摸事件。
響應者鏈條:由不少響應者連接在一塊兒組合起來的一個鏈條稱之爲響應者鏈條

處理原則

響應者鏈條其實還包括視圖控制器、UIWindow和UIApplication,上述例子並無表現出來。以下圖所示:

3.png


個人理解:經過事件傳遞找到最合適的處理觸摸事件的view後(就是最後一個pointInside返回YES的view,它是第一響應者),若是該view是控制器view,那麼上一個響應者就是控制器。若是它不是控制器view,那麼上一個響應者就是前面一個pointInside返回YES的view(其實就是它的父控件)。 最後這些全部pointInside返回YES的view加上它們的控制器、UIWindow和UIApplication共同構成響應者鏈條。響應者鏈條是自上而下的(我把window上最外面的那個view稱爲上),前面的事件傳遞是自下而上的。

響應者鏈條的做用

可讓一個觸摸事件讓多個響應者同時處理該事件。
上面可以在多個view內打印出touchBegan就是利用了此做用,

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchBegan---%@", [self class]);
    [super touchesBegan:touches withEvent:event];
}

5、hitTest:和pointInside:的使用

屏蔽

由前面例子的打印結果與分析能夠看出,不管點擊哪裏,blackView的pointInside方法返回都是YES。
若是將其pointInside返回值改成NO,則其hitTest方法直接返回空,這個屏幕上有色區域都不接收觸摸事件。

若是將greenView的pointInside方法返回YES,會影響上面的五、6變成:
2016-05-27 13:50:45.448 hitTest----BlackView (pointInside返回YES)
2016-05-27 13:50:45.448 hitTest----YellowView (pointInside返回NO)
2016-05-27 13:50:45.448 hitTest----GreenView (pointInside返回YES)
2016-05-27 13:50:45.449 touchBegan—GreenView
2016-05-27 13:50:45.449 touchBegan---BlackView
若是將greenView的pointInside方法返回NO,會影響二、4,綠色區域再也不響應觸摸事件,都交給紅色區域處理:

若是將yellowView的pointInside方法返回YES,會影響上面的二、四、五、6。其中2變成
2016-05-27 11:02:19.268 hitTest----BlackView (pointInside返回YES)
2016-05-27 11:02:19.268 hitTest----YellowView (pointInside返回YES)
2016-05-27 11:02:19.268 hitTest----WhiteView (pointInside返回YES)
2016-05-27 11:02:19.270 touchBegan---WhiteView
2016-05-27 11:02:19.270 touchBegan---YellowView
2016-05-27 11:02:19.270 touchBegan---BlackView
四、五、6變成,
2016-05-27 11:07:18.131 hitTest----BlackView (pointInside返回YES)
2016-05-27 11:07:18.131 hitTest----YellowView (pointInside返回YES)
2016-05-27 11:07:18.131 hitTest----WhiteView (pointInside返回NO)
2016-05-27 11:07:18.133 touchBegan---YellowView
2016-05-27 11:07:18.133 touchBegan---BlackView
若是將yellowView的pointInside方法返回NO,黃色和白色區域再也不響應觸摸,交給後面區域響應。

其他的狀況再也不列出,總結:
若是將某個view的pointInsdie方法直接返回NO(不管子控件的pointInsdie返回什麼),影響的是子控件區域和自身區域的點擊事件處理,這些區域再也不響應事件。其他區域響應點擊事件不發生變化。
若是將某個view的pointInside方法直接返回YES,自身區域響應點擊事件不變。其它改變:
首先,父控件全部區域點擊事件交給該view處理。
而後,再看該view處於父控件的子控件數組中的位置。數組前面的兄弟控件的點擊事件交給該view處理,數組後面的兄弟控件的點擊事件由其兄弟控件處理。
最後,該view的子控件原來可以本身處理點擊的區域繼續由子控件處理,子控件原來不可以本身處理點擊的(超出了該view範圍)區域能夠由子控件處理了。
因此,想要屏蔽掉某個view響應點擊事件,若是其沒有子控件或者子控件響應事件也想屏蔽掉,直接將該view的pointInside返回爲NO就好了。而在通常狀況下,不建議將view的pointInside直接返回YES(影響範圍太廣,很差控制)。

穿透

仍是看前面例子的圖片樣式,如今的要求是想點擊覆蓋在黃色區域的白色區域,點擊事件由yellowView處理,點擊超出黃色區域的白色區域,點擊事件由whiteView本身處理。
1.若是whiteView是yellowView的兄弟控件。
能夠重寫whiteView裏面的hitTest方法:判斷觸摸在whiteView上的點,若是在yellowView上,hitTest返回yellowView,交給其響應。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint yellowPoint = [self convertPoint:point toView:_yellowView];
    if ([_yellowView pointInside:yellowPoint withEvent:event]) {
         return _yellowView;
    }

    return [super hitTest:point withEvent:event];
}

也能夠重寫whiteView的pointInside方法:若是觸摸點屬於yellowView範圍,返回NO,該範圍內whiteView不響應點擊。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
     CGPoint yellowPoint =[_yellowView convertPoint:point fromView:self];
     if ([_yellowView pointInside:yellowPoint withEvent:event]) return NO;

     return [super pointInside:point withEvent:event];
}

2.若是whiteView是yellowView的子控件。
須要重寫whiteView裏面的hitTest方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"hitTest----%@", [self class]);
    CGPoint yellowPoint = [self convertPoint:point toView:_yellowView];
    if ([_yellowView pointInside:yellowPoint withEvent:event]) {
        return _yellowView;
    }

    return [super hitTest:point withEvent:event];
 }

和greenView裏面的hitTest方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
     NSLog(@"hitTest----%@", [self class]);
     CGPoint whitePoint = [self convertPoint:point toView:_whiteView];
     if ([_whiteView pointInside:whitePoint withEvent:event]) {
        return _whiteView;
     }

     return [super hitTest:point withEvent:event];
 }

究竟何時重寫hitTest,何時重寫pointInside,在哪一個view內重寫它們?

不少狀況下hitTest和pointInside方法任選其一均可以實現某個功能,好比在屏蔽中,pointInside返回NO能夠實現的話,均可以用hitTest返回nil代替。
可是,hitTest更強大。由於pointInside在通常狀況下其內部頂多只能根據狀況判斷怎麼返回NO,屏蔽掉本身和子控件的事件響應。因此只要是想保留子控件對觸摸事件響應,屏蔽其父控件的響應,單獨重寫pointInside沒法辦到,必需要重寫hitTest方法。

觸摸事件本來該由某個view響應,如今你不想讓它處理而讓別的控件處理,那麼就應該在該view內重寫hitTest或pointInside方法。

Demo下載地址:https://github.com/wobangnidashui/hitTestDemo

相關文章
相關標籤/搜索