文章目錄
? 一、什么是hitTest
? 二、hitTest的調用順序
? 三、事件的傳遞順序
? 四、hitTest的實現(xiàn)思路
? 五、hitTest的運用場景
? 1、事件穿透
? 2、子視圖超出父視圖范圍
一、什么是hitTest
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
point? ? ? : 在接收器的局部坐標系(界)中指定的點。
event? ? ? : 系統(tǒng)保證調用此方法的事件。如果從事件處理代碼外部調用此方法,則可以指定nil。
returnValue : 視圖對象是當前視圖和包含點的最遠的后代。如果點完全位于接收方的視圖層次結構之外,則返回nil。
hitTest: withEvent: 是UIView 里面的一個方法,該方法的作用 在于 : 在視圖的層次結構中尋找一個最適合的 view 來響應觸摸事件。
該方法會被系統(tǒng)調用,調用的時候,如果返回為nil,即事件有可能被丟棄,否則返回最合適的view 來響應事件。(怎么感覺這句話這么別扭,你們覺得呢?)
二、hitTest 的調用順序
touch -> UIApplication -> UIWindow -> UIViewController.view -> subViews -> ....-> 合適的view
? ? 1
三、事件的傳遞順序
事件傳遞順序與hitTest 的調用順序 恰好相反
view -> superView ...- > UIViewController.view -> UIViewController -> UIWindow -> UIApplication -> 事件丟棄
? ? 1
文字說明:
1、 首先由 view 來嘗試處理事件,如果他處理不了,事件將被傳遞到他的父視圖superview
2、superview 也嘗試來處理事件,如果他處理不了,繼續(xù)傳遞他的父視圖
UIViewcontroller.view
3、UIViewController.view嘗試來處理該事件,如果處理不了,將把該事件傳遞給UIViewController
4、UIViewController嘗試處理該事件,如果處理不了,將把該事件傳遞給主窗口Window
5、主窗口Window嘗試來處理該事件,如果處理不了,將傳遞給應用單例Application
6、如果Application也處理不了,則該事件將會被丟棄
蘋果官方提供的示意圖如下 :

四、hitTest的實現(xiàn)思路
常見的視圖不響應事件不外乎如下幾種情況
1、view.userInteractionEnabled = NO;
2、view.hidden = YES;
3、view.alpha < 0.05
4、view 超出 superview 的 bounds
那么hitTest 就可根據(jù)上面 結果 大概模擬下 hitTest 方法的大概實現(xiàn)
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
? ? // 如果交互未打開,或者透明度小于0.05 或者 視圖被隱藏
? ? if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
? ? {
? ? ? ? return nil;
? ? }
? ? // 如果 touch 的point 在 self 的bounds 內
? ? if ([self pointInside:point withEvent:event])
? ? {
? ? ? ? for (UIView *subView in self.subviews)
? ? ? ? {
? ? ? ? ? ? //進行坐標轉化
? ? ? ? ? ? CGPoint coverPoint = [subView convertPoint:point fromView:self];
? ? ? ? ? // 調用子視圖的 hitTest 重復上面的步驟。找到了,返回hitTest view ,沒找到返回有自身處理
? ? ? ? ? ? UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
? ? ? ? ? ? if (hitTestView)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return hitTestView;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return self;
? ? }
? ? return nil;
}
步驟文字說明
1、首先在當前視圖的hitTest方法中調用pointInside方法判斷觸摸點是否在當前視圖內
2、若pointInside方法返回NO,說明觸摸點不在當前視圖內,則當前視圖的hitTest返回nil,該視圖不處理該事件
3、若pointInside方法返回YES,說明觸摸點在當前視圖內,則從最上層的子視圖開始(即從subviews數(shù)組的末尾向前遍歷),遍歷當前視圖的所有子視圖,調用子視圖的hitTest方法重復步驟1-3
4、直到有子視圖的hitTest方法返回非空對象或者全部子視圖遍歷完畢
5、若第一次有子視圖的hitTest方法返回非空對象,則當前視圖的hitTest方法就返回此對象,處理結束
6、若所有子視圖的hitTest方法都返回nil,則當前視圖的hitTest方法返回當前視圖本身,最終由該對象處理觸摸事件
結合下面的例子理解上面的步驟 :

其中 : 紅點為touch 點
當點擊ViewE時,hitTest執(zhí)行順序如下:
先看看點擊大致走向圖如下,其中,?部分為執(zhí)行pointInside為YES部分,X部分執(zhí)行pointInside為NO部分,最終hitTest返回ViewE。

1、首先調用ViewA的hitTest方法,由于觸摸點在其范圍內,pointInside返回YES,遍歷其子視圖,依次調用ViewB和ViewC的hitTest方法
2、執(zhí)行ViewB的hitTest方法,由于觸摸點是不在ViewB內,其pointInside方法返回NO,hitTest返回nil
3、執(zhí)行ViewC的hitTest方法,由于觸摸點是在ViewC內,其pointInside方法返回YES,遍歷其子視圖,依次調用ViewD和ViewE的hitTest方法
4、執(zhí)行ViewD的hitTest方法,由于觸摸點是不在ViewD內,其pointInside方法返回NO,所以其hitTest返回nil
5、執(zhí)行ViewE的hitTest方法,由于觸摸點是在 ViewE內,其pointInside方法返回YES,由于其沒有子視圖了,其hitTest返回其本身
6、最終,由ViewE來響應該點擊事件
五、hitTest的運用場景
1、事件穿透

青色的遮罩層(maskView) 在黃色 按鈕(btn) 的上層,即 視圖的添加順序為,先添加 黃色 按鈕(btn),再添加 青色的遮罩(maskView)。 有這么個需求,點擊按鈕修改maskView 的背景顏色。
注意 : btn 和 maskview 有重疊部分。視圖都添加到UIViewController.view 。
1>、首先,調用UIViewController.view的hitTest。
2>、其次,遍歷子視圖,進行坐標轉化,判斷 point 是否在 bounds 內,發(fā)現(xiàn) maskview 和 btn 都滿足
3>、再者,調用maskview 和 btn 的hitTest 方法,到這里,我們的目標的hitText View 很顯然是btn,那么我們自然就很容易想到,根據(jù) maskview 的isa 找到 類對象,在類對象 重寫 hitTest 方法,當hitTestview == self ,返回nil 即可。這樣,事件就別 btn 捕獲到。代碼如下:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
? ? UIView *hitTestView = [super hitTest:point withEvent:event];
? ? if (hitTestView == self)
? ? {
? ? ? ? return nil;
? ? }else
? ? {
? ? ? ? return hitTestView;
? ? }
}
2、子視圖超出父視圖 范圍

發(fā)布按鈕已然已經(jīng)超出tabbar的范圍,那么該按鈕是如何響應點擊事件的?
要讓中間按鈕響應點擊超出TabBar按鈕部分的點擊事件,則需要重寫TabBar的hitTest方法了,在執(zhí)行hitTest方法時,判斷點擊區(qū)域在中間按鈕的區(qū)域,則返回中間按鈕,響應該事件,代碼如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? //將當前tabbar的觸摸點轉換坐標系,轉換到中間按鈕的身上,生成一個新的點
? ? CGPoint newP = [self convertPoint:point toView:self.centerBtn];
? ? ? //判斷如果這個新的點是在中間按鈕身上,那么處理點擊事件最合適的view就是中間按鈕
? ? ? if ( [self.centerBtn pointInside:newP withEvent:event])
? ? ? {
? ? ? ? ? ? return self.centerBtn;
? ? ? }
? ? return [super hitTest:point withEvent:event];
}//重寫hitTest方法,去監(jiān)聽中間按鈕的點擊,目的是為了讓凸出的部分點擊也有反應