在這篇文章里句占,我們將探討如何使用 hitTest:withEvent:方法來自定義view的響應區(qū)域.
拋出問題
項目中需要實現(xiàn)實時自動補全用戶輸入的功能纪蜒,也就是說在 textField (用戶輸入框)下方需要實時展示出根據(jù)用戶輸入而自動補全的人名列表或者其他列表.如下圖所示:
整個viewcontroller是被一個UICollectionView填充的澄干,在該UICollectionView上有一個touch事件吊洼,用來dismiss keyboard强法。而整個的輸入框view(包含背景)都是添加在UICollectionView中的supplementary view(header view, section equals 1)蟹肘,所以,當autoCompletionTableview(自動補全列表)出現(xiàn)時楣导,會超出其superview的邊界(由于未設置clipToBounds=YES废境,所以超出部分可以看到)。雖然超出的部分可見筒繁,但是去無法正常響應tableview的touch事件噩凹。那如何解決這個問題呢?
原理分析
知己知彼毡咏,百戰(zhàn)不殆驮宴。我們需要了解他的原理,才能對癥下藥呕缭。
在iOS系統(tǒng)中堵泽,存在一個名為“Response chain”(響應者鏈條)的過程. 通俗的解釋就是:系統(tǒng)會在視圖層次結(jié)構(gòu)中找到一個最合適的視圖來處理觸摸事件(pan, pinch, tap, etc). 那系統(tǒng)是如何去尋找的呢?那些方法起了作用呢恢总?
首先迎罗,在響應者鏈條中的“候選者”都會直接或間接的繼承 UIResponder 這個基類,以確保他們的實例可以響應和處理用戶touch事件片仿,例如我們耳熟能詳?shù)?UIApplication纹安、 UIViewController、UIWindow和所有繼承自UIView的UIKit類. 下圖展示了響應者鏈的基本構(gòu)成:
從圖中可以看出響應者鏈有如下特征:
- 響應者鏈通常是由視圖(UIView)和控制器(view controller)構(gòu)成的;
- 一個視圖的下一個響應者是它視圖控制器(view controller(如果有的話),然后再轉(zhuǎn)給它的父視圖(Super View);
- 如果遍歷完所有view和view controller后依舊沒有發(fā)現(xiàn)可響應的組件, 那么單例的內(nèi)容視圖UIWindow將作為下一個響應者;
- 最后UIApplication這個"上帝類"將作為響應者鏈的終點結(jié)束整個循環(huán).
事件分發(fā)機制
上一部分是"自底向上"的響應機制, 下面我們來說說"自頂向下"的分發(fā)機制.
整個的響應開端是從UIApplication控制的NSRunloop開始的钻蔑,NSRunloop監(jiān)聽到用戶的touch event(存放在UIApplication的事件隊列中)之后啥刻,就開始了消息的分發(fā), UIWindow是第一個接受這個消息的對象,并以消息的形式將事件發(fā)送給第一響應者咪笑,使其有機會首先處理事件可帽。如果第一響應者沒有進行處理,系統(tǒng)就將事件(通過消息)傳遞給響應者鏈中的下一個響應者窗怒,看看它是否可以進行處理映跟。在這個過程中,UIWindow是通過hitTest:withEvent:方法尋找此次Touch操作初始點所在的視圖(View), 即需要將觸摸事件傳遞給其處理的視圖扬虚,這個過程稱之為hit-test view努隙。
hitTest:withEvent:方法處理流程如下:
首先調(diào)用當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內(nèi);之后若返回NO辜昵,則hitTest:withEvent:返回NO荸镊,結(jié)束循環(huán); 若為YES,則向當前視圖的所有子視圖(subviews)發(fā)送hitTest:withEvent:消息堪置,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖躬存,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對象或者全部子視圖遍歷完畢舀锨;若第一次有子視圖返回非空對象岭洲,則hitTest:withEvent:方法返回此對象,處理結(jié)束坎匿;如所有子視圖都返回空盾剩,則hitTest:withEvent:方法返回自身(self)。
下面用一個例子說明此流程:
用戶點擊View E替蔬,hit-test view流程如下:
A是UIWindow的根視圖告私,因此,UIWindow對象會首先對A進行hit-test进栽;
顯然用戶點擊的范圍是在A的范圍內(nèi)德挣,因此,pointInside:withEvent:返回了YES快毛,這時會繼續(xù)檢查A的子視圖;
這時候會有兩個分支番挺,B和C, 點擊的范圍不再B內(nèi)唠帝,因此B分支的pointInside:withEvent:返回NO,對應的hitTest:withEvent:返回nil玄柏;
點擊的范圍在C內(nèi)襟衰,即C的pointInside:withEvent:返回YES;這時候有D和E兩個分支:點擊的范圍不再D內(nèi)粪摘,因此D的pointInside:withEvent:返回NO瀑晒,對應的
hitTest:withEvent:返回nil绍坝;點擊的范圍在E內(nèi),即E的pointInside:withEvent:返回YES苔悦,由于E沒有子視圖(也可以理解成對E的子視圖進行hit-test時返回
了nil)轩褐,因此,E的hitTest:withEvent:會將E返回玖详,再往回回溯把介,就是C的hitTest:withEven:t方法; 之后返回A的hitTest:withEvent:方法。
至此蟋座,本次點擊事件的第一響應者就通過響應者鏈的事件分發(fā)邏輯成功的找到了拗踢。而且不難看出,這個處理流程有點類似Binary Search的思想向臀,這樣能以最快的速度巢墅,最精確地定位出能響應觸摸事件的UIView。
實戰(zhàn)
在知道了原理后, 我們就要著手解決文章一開始提出的問題了. 問題的關(guān)鍵就在于override hitTest:withEvent 和 pointInside:withEvent: 這兩個方法券膀。
當點擊超出部分的table view區(qū)域時, 首先君纫,其super view 的pointInside:withEvent: 方法會返回NO, 這也直接導致 hitTest:withEvent 返回nil給上層三娩。解決方案可以很簡單, 找到對應的view庵芭,重寫pointInside:withEvent:方法(計算超出部分區(qū)域,判斷后返回YES雀监;或者直接返回YES)双吆。之后, 再重寫hitTest:withEvent:, 因為其默認返回值是包含autoCompletionTableView的container view,并不是table view会前, 所以在這里要轉(zhuǎn)換坐標系好乐,并將返回的view改為超出super view邊界的table view。按照這個步驟瓦宜,超出部分的view就可以響應touch事件了.
代碼如下:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint hitPoint = [self.autoCompletionTableView convertPoint:point fromView:self];
if ([self.autoCompletionTableView pointInside:hitPoint withEvent:event]) {
return self.autoCompletionTableView;
}
return [super hitTest:point withEvent:event];
}