之前在文章《iOS-實現(xiàn)映客首頁TabBar和滑動隱藏NavBar和TabBar》中驶沼,提到了
hitTest
方法夯秃,但是沒有詳細(xì)說明,導(dǎo)致有童鞋不理解為什么要這么做苇经,這幾天把hitTest
的資料整理了一下,在這里介紹一些宦言,解開疑惑扇单。
這篇文章,最終的目的就是解釋如何讓中間按鈕超出TabBar
部分響應(yīng)點擊事件奠旺。效果圖如下:
這篇文章將圍繞一下幾個問題來講:
-
hitTest
是什么 -
hitTest
的調(diào)用順序是怎么樣的 -
hitTest
和事件傳遞有什么關(guān)系 -
hitTest
是如何解決子視圖超出其視圖范圍還是能響應(yīng)觸摸事件的
下面我們一個一個來看蜘澜。
1. hitTest是什么
hitTest:withEvent:
是UIView
的一個方法,該方法會被系統(tǒng)調(diào)用响疚,是用于在視圖(UIView
)層次結(jié)構(gòu)中找到一個最合適的UIView
來響應(yīng)觸摸事件鄙信。
2. hitTest的調(diào)用順序是怎么樣的
一個觸摸事件事件傳遞順序大致如下:
touch->UIApplication->UIWindow->UIViewController.view->subViews->...->view
1) 觸摸事件傳遞順序
- 當(dāng)用戶點擊屏幕時怖糊,會產(chǎn)生一個觸摸事件脆烟,系統(tǒng)會將該事件加入到由
UIApplication
管理的事件隊列中 -
UIApplication
會從事件隊列中取出最早的事件進行分發(fā)處理,先發(fā)送事件給應(yīng)用程序的主窗口UIWindow
- 主窗口會調(diào)用其
hitTest:withEvent:
方法在視圖(UIView
)層次結(jié)構(gòu)中找到一個最合適的UIView
來處理觸摸事件
2) hitTest調(diào)用順序
以下pointInside:withEvent:
簡稱為pointInside
圈膏,hitTest:withEvent:
簡稱為hitTest
hitTest
的代碼邏輯大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//系統(tǒng)默認(rèn)會忽略isUserInteractionEnabled設(shè)置為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;
}
執(zhí)行順序如下:
- 首先在當(dāng)前視圖的
hitTest
方法中調(diào)用pointInside
方法判斷觸摸點是否在當(dāng)前視圖內(nèi) - 若
pointInside
方法返回NO
,說明觸摸點不在當(dāng)前視圖內(nèi)咕幻,則當(dāng)前視圖的hitTest
返回nil
渔伯,該視圖不處理該事件 - 若
pointInside
方法返回YES
,說明觸摸點在當(dāng)前視圖內(nèi)肄程,則從最上層的子視圖開始(即從subviews
數(shù)組的末尾向前遍歷)锣吼,遍歷當(dāng)前視圖的所有子視圖,調(diào)用子視圖的hitTest
方法重復(fù)步驟1-3
- 直到有子視圖的
hitTest
方法返回非空對象或者全部子視圖遍歷完畢 - 若第一次有子視圖的
hitTest
方法返回非空對象蓝厌,則當(dāng)前視圖的hitTest
方法就返回此對象玄叠,處理結(jié)束 - 若所有子視圖的
hitTest
方法都返回nil
,則當(dāng)前視圖的hitTest
方法返回當(dāng)前視圖本身拓提,最終由該對象處理觸摸事件
上面的流程诸典,看著可能有點繞,我們來看下面一個例子
上圖中有5個View
崎苗,紅點為手指點擊區(qū)域狐粱,ViewA
為父視圖,ViewB
和ViewC
為ViewA
的子視圖胆数,ViewD
和ViewE
為ViewC
的子視圖肌蜻。
(這里假設(shè)所有View
都可以響應(yīng)點擊事件,而且ViewB
在ViewC
上層必尼,ViewD
在ViewE
上層蒋搜,即ViewB
的addSubView:
執(zhí)行在ViewC
之后,ViewD
的addSubView:
執(zhí)行在ViewE
之后)
當(dāng)點擊ViewE
時判莉,hitTest
執(zhí)行順序如下:
先看看點擊大致走向圖如下豆挽,其中,?部分為執(zhí)行pointInside
為YES
部分券盅,X
部分執(zhí)行pointInside
為NO
部分帮哈,最終hitTest
返回ViewE
- 首先調(diào)用
ViewA
的hitTest
方法,由于觸摸點在其范圍內(nèi)锰镀,pointInside
返回YES
娘侍,遍歷其子視圖,依次調(diào)用ViewB
和ViewC
的hitTest
方法 - 執(zhí)行
ViewB
的hitTest
方法泳炉,由于觸摸點是不在ViewB
內(nèi)憾筏,其pointInside
方法返回NO
,hitTest
返回nil
- 執(zhí)行
ViewC
的hitTest
方法花鹅,由于觸摸點是在ViewC
內(nèi)氧腰,其pointInside
方法返回YES
,遍歷其子視圖刨肃,依次調(diào)用ViewD
和ViewE
的hitTest
方法 - 執(zhí)行
ViewD
的hitTest
方法古拴,由于觸摸點是不在ViewD
內(nèi),其pointInside
方法返回NO
之景,所以其hitTest
返回nil
- 執(zhí)行
ViewE
的hitTest
方法斤富,由于觸摸點是在ViewE
內(nèi),其pointInside
方法返回YES
锻狗,由于其沒有子視圖了满力,其hitTest
返回其本身 - 最終,由
ViewE
來響應(yīng)該點擊事件
3. hitTest和事件傳遞有什么關(guān)系
事件傳遞的的順序和hitTest
中pointInside
返回為YES
的視圖的執(zhí)行順序是相反的轻纪。事件傳遞是從最上層的視圖開始傳遞的油额,直到UIApplication
。
拿我們上面的例子來說刻帚,hitTest
執(zhí)行的結(jié)果是ViewE
來響應(yīng)事件潦嘶,但是如果ViewE
并不處理該事件,則其需要把該事件進行傳遞給下一個響應(yīng)者崇众,這個時候掂僵,它會將事件拋給ViewC
航厚,如果ViewC
也不處理事件,則其會將事件傳遞給ViewA
锰蓬,如果ViewA
也不處理幔睬,則該事件就不響應(yīng)了。
以下由蘋果官方文檔提供的事件傳遞圖
上圖事件的傳遞流程如下:
- 首先芹扭,由
initial view
嘗試來處理事件麻顶,如果它處理不了,則會將事件傳遞給他的父視圖View
-
View
嘗試處理該事件舱卡,如果其也處理不了辅肾,再傳遞給它的父視圖UIViewController.view
-
UIViewController.view
嘗試來處理該事件,如果處理不了轮锥,將把該事件傳遞給UIViewController
-
UIViewController
嘗試處理該事件矫钓,如果處理不了,將把該事件傳遞給主窗口Window
- 主窗口
Window
嘗試來處理該事件交胚,如果處理不了份汗,將傳遞給應(yīng)用單例Application
- 如果應(yīng)用單例
Application
也處理不了,則該事件將會被丟棄
4. hitTest是如何解決子視圖超出其視圖范圍還是能響應(yīng)觸摸事件的
我們來看看下面的圖蝴簇,下圖中中間按鈕超出了TabBar
的區(qū)域
我們通過
Xcode
中下圖紅框按鈕來查看該頁面的層級關(guān)系我來看下這個圖的層級關(guān)系
從以上圖可以看出杯活,TabBar
和UITableView
,共同的父類為UILayoutContainerView
熬词,而TabBar
的層級旁钧,相對于UITableView
高些,它和UITransitionView
是同級的互拾。
當(dāng)我們點擊中間按鈕超出TabBar
部分(“中間按鈕超出了TabBar
的區(qū)域效果圖”紅框部分)歪今,系統(tǒng)是如何處理的呢?我們跳過UIWindow
颜矿,直接從UILayoutContainerView
開始調(diào)用hitTest
寄猩。
先看看大致走向圖,其中骑疆,?部分為執(zhí)行pointInside
為YES
部分田篇,X
部分執(zhí)行pointInside
為NO
部分
- 調(diào)用
UILayoutContainerView
的hitTest
方法,由于是在其區(qū)域內(nèi)箍铭,pointInside
返回YES
泊柬,再遍歷其子視圖,調(diào)用hitTest
- 先調(diào)用
TabBar
的hitTest
方法诈火,由于點擊區(qū)域是在TabBar
之外的兽赁,所以pointInside
返回NO
,hitTest
返回nil
,TabBar
并不響應(yīng)該事件 - 再調(diào)用
UITransitionView
的hitTest
方法刀崖,在其區(qū)域內(nèi)惊科,遞歸調(diào)用子視圖hitTest
方法,直到調(diào)用UITableView
在突出按鈕后的UITableViewCell
的hitTest
返回蒲跨,返回該Cell
译断,最終由Cell
響應(yīng)該事件
所以,系統(tǒng)默認(rèn)的處理方式或悲,超出TabBar
區(qū)域,中間按鈕是不響應(yīng)該事件的堪唐,而是由其后視圖響應(yīng)巡语。
想要超出父視圖區(qū)域響應(yīng)點擊事件,必須將走向圖該為如下所示(其中淮菠,?部分為執(zhí)行pointInside
為YES
部分男公,X
部分執(zhí)行pointInside
為NO
部分):
要讓中間按鈕響應(yīng)點擊超出TabBar
按鈕部分的點擊事件,則需要重寫TabBar
的hitTest
方法了合陵,在執(zhí)行hitTest
方法時枢赔,判斷點擊區(qū)域在中間按鈕的區(qū)域,則返回中間按鈕拥知,響應(yīng)該事件踏拜,代碼如下:
//重寫hitTest方法,去監(jiān)聽中間按鈕的點擊低剔,目的是為了讓凸出的部分點擊也有反應(yīng)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//判斷當(dāng)前手指是否點擊到中間按鈕上速梗,如果是,則響應(yīng)按鈕點擊襟齿,其他則系統(tǒng)處理
//首先判斷當(dāng)前View是否被隱藏了姻锁,隱藏了就不需要處理了
if (self.isHidden == NO) {
//將當(dāng)前tabbar的觸摸點轉(zhuǎn)換坐標(biāo)系,轉(zhuǎn)換到中間按鈕的身上猜欺,生成一個新的點
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
直接來響應(yīng)點擊事件开皿,行不行呢涧黄?
答:答案當(dāng)然是不行的,如果你看懂了這篇文章副瀑,那就知道答案了弓熏。如果不在TabBar
中重寫hitTest
方法,系統(tǒng)是先調(diào)用TabBar
的hitTest
方法的糠睡,在調(diào)用該hitTest
方法時挽鞠,判斷點擊超出TabBar
部分,不在其區(qū)域內(nèi),pointInside
就返回NO
了信认,hitTest
直接返回nil
材义,TabBar
不能響應(yīng)該事件,其子視圖(中間按鈕)也就沒機會執(zhí)行hitTest
方法了嫁赏。所以是不行的其掂。
參考文章
如果覺得該文章對你有幫助,請幫忙點贊潦蝇,如果發(fā)現(xiàn)有錯誤款熬,請幫忙指出,謝謝攘乒!