iOS-使用hitTest控制點選事件的響應物件
之前在文章《iOS-實現映客首頁TabBar和滑動隱藏NavBar和TabBar》中,提到了
hitTest
方法,但是沒有詳細說明,導致有童鞋不理解為什麼要這麼做,這幾天把hitTest
的資料整理了一下,在這裡介紹一些,解開疑惑。
這篇文章,最終的目的就是解釋如何讓中間按鈕超出TabBar
部分響應點選事件。效果圖如下:
中間按鈕超出了`TabBar`的區域效果圖
這篇文章將圍繞一下幾個問題來講:
hitTest
是什麼hitTest
的呼叫順序是怎麼樣的hitTest
和事件傳遞有什麼關係hitTest
是如何解決子檢視超出其檢視範圍還是能響應觸控事件的
下面我們一個一個來看。
1. hitTest是什麼
hitTest:withEvent:
是UIView
的一個方法,該方法會被系統呼叫,是用於在檢視(UIView
)層次結構中找到一個最合適的UIView
來響應觸控事件。
2. hitTest的呼叫順序是怎麼樣的
一個觸控事件事件傳遞順序大致如下:
touch->UIApplication->UIWindow->UIViewController.view->subViews->...->view
1) 觸控事件傳遞順序
- 當用戶點選螢幕時,會產生一個觸控事件,系統會將該事件加入到由
UIApplication
UIApplication
會從事件佇列中取出最早的事件進行分發處理,先發送事件給應用程式的主視窗UIWindow
- 主視窗會呼叫其
hitTest:withEvent:
方法在檢視(UIView
)層次結構中找到一個最合適的UIView
來處理觸控事件
2) hitTest呼叫順序
以下pointInside:withEvent:
簡稱為pointInside
,hitTest:withEvent:
簡稱為hitTest
hitTest
的程式碼邏輯大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { //系統預設會忽略isUserInteractionEnabled設定為NO、隱藏、alpha小於等於0.01的檢視 if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; }
執行順序如下:
- 首先在當前檢視的
hitTest
方法中呼叫pointInside
方法判斷觸控點是否在當前檢視內 - 若
pointInside
方法返回NO
,說明觸控點不在當前檢視內,則當前檢視的hitTest
返回nil
,該檢視不處理該事件 - 若
pointInside
方法返回YES
,說明觸控點在當前檢視內,則從最上層的子檢視開始(即從subviews
陣列的末尾向前遍歷),遍歷當前檢視的所有子檢視,呼叫子檢視的hitTest
方法重複步驟1-3
- 直到有子檢視的
hitTest
方法返回非空物件或者全部子檢視遍歷完畢 - 若第一次有子檢視的
hitTest
方法返回非空物件,則當前檢視的hitTest
方法就返回此物件,處理結束 - 若所有子檢視的
hitTest
方法都返回nil
,則當前檢視的hitTest
方法返回當前檢視本身,最終由該物件處理觸控事件
上面的流程,看著可能有點繞,我們來看下面一個例子
例子
上圖中有5個View
,紅點為手指點選區域,ViewA
為父檢視,ViewB
和ViewC
為ViewA
的子檢視,ViewD
和ViewE
為ViewC
的子檢視。
(這裡假設所有View
都可以響應點選事件,而且ViewB
在ViewC
上層,ViewD
在ViewE
上層,即ViewB
的addSubView:
執行在ViewC
之後,ViewD
的addSubView:
執行在ViewE
之後)
當點選ViewE
時,hitTest
執行順序如下:
先看看點選大致走向圖如下,其中,✅部分為執行pointInside
為YES
部分,X
部分執行pointInside
為NO
部分,最終hitTest
返回ViewE
hitTest走向圖
- 首先呼叫
ViewA
的hitTest
方法,由於觸控點在其範圍內,pointInside
返回YES
,遍歷其子檢視,依次呼叫ViewB
和ViewC
的hitTest
方法 - 執行
ViewB
的hitTest
方法,由於觸控點是不在ViewB
內,其pointInside
方法返回NO
,hitTest
返回nil
- 執行
ViewC
的hitTest
方法,由於觸控點是在ViewC
內,其pointInside
方法返回YES
,遍歷其子檢視,依次呼叫ViewD
和ViewE
的hitTest
方法 - 執行
ViewD
的hitTest
方法,由於觸控點是不在ViewD
內,其pointInside
方法返回NO
,所以其hitTest
返回nil
- 執行
ViewE
的hitTest
方法,由於觸控點是在ViewE
內,其pointInside
方法返回YES
,由於其沒有子檢視了,其hitTest
返回其本身 - 最終,由
ViewE
來響應該點選事件
3. hitTest和事件傳遞有什麼關係
事件傳遞的的順序和hitTest
中pointInside
返回為YES
的檢視的執行順序是相反的。事件傳遞是從最上層的檢視開始傳遞的,直到UIApplication
。
拿我們上面的例子來說,hitTest
執行的結果是ViewE
來響應事件,但是如果ViewE
並不處理該事件,則其需要把該事件進行傳遞給下一個響應者,這個時候,它會將事件拋給ViewC
,如果ViewC
也不處理事件,則其會將事件傳遞給ViewA
,如果ViewA
也不處理,則該事件就不響應了。
以下由蘋果官方文件提供的事件傳遞圖
蘋果官方提供的事件傳遞圖
上圖事件的傳遞流程如下:
- 首先,由
initial view
嘗試來處理事件,如果它處理不了,則會將事件傳遞給他的父檢視View
View
嘗試處理該事件,如果其也處理不了,再傳遞給它的父檢視UIViewController.view
UIViewController.view
嘗試來處理該事件,如果處理不了,將把該事件傳遞給UIViewController
UIViewController
嘗試處理該事件,如果處理不了,將把該事件傳遞給主視窗Window
- 主視窗
Window
嘗試來處理該事件,如果處理不了,將傳遞給應用單例Application
- 如果應用單例
Application
也處理不了,則該事件將會被丟棄
4. hitTest是如何解決子檢視超出其檢視範圍還是能響應觸控事件的
我們來看看下面的圖,下圖中中間按鈕超出了TabBar
的區域
中間按鈕超出了`TabBar`的區域效果圖
我們通過Xcode
中下圖紅框按鈕來檢視該頁面的層級關係
Xcode中層級檢視按鈕
我來看下這個圖的層級關係
層級結構圖
從以上圖可以看出,TabBar
和UITableView
,共同的父類為UILayoutContainerView
,而TabBar
的層級,相對於UITableView
高些,它和UITransitionView
是同級的。
當我們點選中間按鈕超出TabBar
部分(“中間按鈕超出了TabBar
的區域效果圖”紅框部分),系統是如何處理的呢?我們跳過UIWindow
,直接從UILayoutContainerView
開始呼叫hitTest
。
先看看大致走向圖,其中,✅部分為執行pointInside
為YES
部分,X
部分執行pointInside
為NO
部分
hitTest走向圖
- 呼叫
UILayoutContainerView
的hitTest
方法,由於是在其區域內,pointInside
返回YES
,再遍歷其子檢視,呼叫hitTest
- 先呼叫
TabBar
的hitTest
方法,由於點選區域是在TabBar
之外的,所以pointInside
返回NO
,hitTest
返回nil
,TabBar
並不響應該事件 - 再呼叫
UITransitionView
的hitTest
方法,在其區域內,遞迴呼叫子檢視hitTest
方法,直到呼叫UITableView
在突出按鈕後的UITableViewCell
的hitTest
返回,返回該Cell
,最終由Cell
響應該事件
所以,系統預設的處理方式,超出TabBar
區域,中間按鈕是不響應該事件的,而是由其後檢視響應。
想要超出父檢視區域響應點選事件,必須將走向圖該為如下所示(其中,✅部分為執行pointInside
為YES
部分,X
部分執行pointInside
為NO
部分):
修改後的hitTest走向圖
要讓中間按鈕響應點選超出TabBar
按鈕部分的點選事件,則需要重寫TabBar
的hitTest
方法了,在執行hitTest
方法時,判斷點選區域在中間按鈕的區域,則返回中間按鈕,響應該事件,程式碼如下:
//重寫hitTest方法,去監聽中間按鈕的點選,目的是為了讓凸出的部分點選也有反應
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//判斷當前手指是否點選到中間按鈕上,如果是,則響應按鈕點選,其他則系統處理
//首先判斷當前View是否被隱藏了,隱藏了就不需要處理了
if (self.isHidden == NO) {
//將當前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
直接來響應點選事件,行不行呢?
答:答案當然是不行的,如果你看懂了這篇文章,那就知道答案了。如果不在TabBar
中重寫hitTest
方法,系統是先呼叫TabBar
的hitTest
方法的,在呼叫該hitTest
方法時,判斷點選超出TabBar
部分,不在其區域內,pointInside
就返回NO
了,hitTest
直接返回nil
,TabBar
不能響應該事件,其子檢視(中間按鈕)也就沒機會執行hitTest
方法了。所以是不行的。
參考文章
作者:HK_Hank
連結:https://www.jianshu.com/p/ca3cd5306668
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。