Hit-test 實(shí)戰(zhàn)

在這篇文章里,我們將探討如何使用 hitTest:withEvent:方法來(lái)自定義view的響應(yīng)區(qū)域.

拋出問(wèn)題

項(xiàng)目中需要實(shí)現(xiàn)實(shí)時(shí)自動(dòng)補(bǔ)全用戶(hù)輸入的功能,也就是說(shuō)在 textField (用戶(hù)輸入框)下方需要實(shí)時(shí)展示出根據(jù)用戶(hù)輸入而自動(dòng)補(bǔ)全的人名列表或者其他列表.如下圖所示:

Image1.png

整個(gè)viewcontroller是被一個(gè)UICollectionView填充的,在該UICollectionView上有一個(gè)touch事件,用來(lái)dismiss keyboard。而整個(gè)的輸入框view(包含背景)都是添加在UICollectionView中的supplementary view(header view, section equals 1),所以,當(dāng)autoCompletionTableview(自動(dòng)補(bǔ)全列表)出現(xiàn)時(shí),會(huì)超出其superview的邊界(由于未設(shè)置clipToBounds=YES,所以超出部分可以看到)。雖然超出的部分可見(jiàn),但是去無(wú)法正常響應(yīng)tableview的touch事件。那如何解決這個(gè)問(wèn)題呢?

原理分析

知己知彼,百戰(zhàn)不殆。我們需要了解他的原理,才能對(duì)癥下藥。

在iOS系統(tǒng)中,存在一個(gè)名為“Response chain”(響應(yīng)者鏈條)的過(guò)程. 通俗的解釋就是:系統(tǒng)會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件(pan, pinch, tap, etc). 那系統(tǒng)是如何去尋找的呢?那些方法起了作用呢?

首先,在響應(yīng)者鏈條中的“候選者”都會(huì)直接或間接的繼承 UIResponder 這個(gè)基類(lèi),以確保他們的實(shí)例可以響應(yīng)和處理用戶(hù)touch事件,例如我們耳熟能詳?shù)?UIApplication、 UIViewController、UIWindow和所有繼承自UIView的UIKit類(lèi). 下圖展示了響應(yīng)者鏈的基本構(gòu)成:

Image2.png

從圖中可以看出響應(yīng)者鏈有如下特征:

  1. 響應(yīng)者鏈通常是由視圖(UIView)和控制器(view controller)構(gòu)成的;
  2. 一個(gè)視圖的下一個(gè)響應(yīng)者是它視圖控制器(view controller(如果有的話),然后再轉(zhuǎn)給它的父視圖(Super View);
  3. 如果遍歷完所有view和view controller后依舊沒(méi)有發(fā)現(xiàn)可響應(yīng)的組件, 那么單例的內(nèi)容視圖UIWindow將作為下一個(gè)響應(yīng)者;
  4. 最后UIApplication這個(gè)"上帝類(lèi)"將作為響應(yīng)者鏈的終點(diǎn)結(jié)束整個(gè)循環(huán).

事件分發(fā)機(jī)制

上一部分是"自底向上"的響應(yīng)機(jī)制, 下面我們來(lái)說(shuō)說(shuō)"自頂向下"的分發(fā)機(jī)制.

整個(gè)的響應(yīng)開(kāi)端是從UIApplication控制的NSRunloop開(kāi)始的,NSRunloop監(jiān)聽(tīng)到用戶(hù)的touch event(存放在UIApplication的事件隊(duì)列中)之后,就開(kāi)始了消息的分發(fā), UIWindow是第一個(gè)接受這個(gè)消息的對(duì)象,并以消息的形式將事件發(fā)送給第一響應(yīng)者,使其有機(jī)會(huì)首先處理事件。如果第一響應(yīng)者沒(méi)有進(jìn)行處理,系統(tǒng)就將事件(通過(guò)消息)傳遞給響應(yīng)者鏈中的下一個(gè)響應(yīng)者,看看它是否可以進(jìn)行處理。在這個(gè)過(guò)程中,UIWindow是通過(guò)hitTest:withEvent:方法尋找此次Touch操作初始點(diǎn)所在的視圖(View), 即需要將觸摸事件傳遞給其處理的視圖,這個(gè)過(guò)程稱(chēng)之為hit-test view。

hitTest:withEvent:方法處理流程如下:

首先調(diào)用當(dāng)前視圖的pointInside:withEvent:方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi);之后若返回NO,則hitTest:withEvent:返回NO,結(jié)束循環(huán); 若為YES,則向當(dāng)前視圖的所有子視圖(subviews)發(fā)送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢;若第一次有子視圖返回非空對(duì)象,則hitTest:withEvent:方法返回此對(duì)象,處理結(jié)束;如所有子視圖都返回空,則hitTest:withEvent:方法返回自身(self)。

下面用一個(gè)例子說(shuō)明此流程:

Image3.png

用戶(hù)點(diǎn)擊View E,hit-test view流程如下:

  1. A是UIWindow的根視圖,因此,UIWindow對(duì)象會(huì)首先對(duì)A進(jìn)行hit-test;

  2. 顯然用戶(hù)點(diǎn)擊的范圍是在A的范圍內(nèi),因此,pointInside:withEvent:返回了YES,這時(shí)會(huì)繼續(xù)檢查A的子視圖;

  3. 這時(shí)候會(huì)有兩個(gè)分支,B和C, 點(diǎn)擊的范圍不再B內(nèi),因此B分支的pointInside:withEvent:返回NO,對(duì)應(yīng)的hitTest:withEvent:返回nil;

  4. 點(diǎn)擊的范圍在C內(nèi),即C的pointInside:withEvent:返回YES;這時(shí)候有D和E兩個(gè)分支:點(diǎn)擊的范圍不再D內(nèi),因此D的pointInside:withEvent:返回NO,對(duì)應(yīng)的
    hitTest:withEvent:返回nil;點(diǎn)擊的范圍在E內(nèi),即E的pointInside:withEvent:返回YES,由于E沒(méi)有子視圖(也可以理解成對(duì)E的子視圖進(jìn)行hit-test時(shí)返回
    了nil),因此,E的hitTest:withEvent:會(huì)將E返回,再往回回溯,就是C的hitTest:withEven:t方法; 之后返回A的hitTest:withEvent:方法。

至此,本次點(diǎn)擊事件的第一響應(yīng)者就通過(guò)響應(yīng)者鏈的事件分發(fā)邏輯成功的找到了。而且不難看出,這個(gè)處理流程有點(diǎn)類(lèi)似Binary Search的思想,這樣能以最快的速度,最精確地定位出能響應(yīng)觸摸事件的UIView。

實(shí)戰(zhàn)

在知道了原理后, 我們就要著手解決文章一開(kāi)始提出的問(wèn)題了. 問(wèn)題的關(guān)鍵就在于override hitTest:withEvent 和 pointInside:withEvent: 這兩個(gè)方法。
當(dāng)點(diǎn)擊超出部分的table view區(qū)域時(shí), 首先,其super view 的pointInside:withEvent: 方法會(huì)返回NO, 這也直接導(dǎo)致 hitTest:withEvent 返回nil給上層。解決方案可以很簡(jiǎn)單, 找到對(duì)應(yīng)的view,重寫(xiě)pointInside:withEvent:方法(計(jì)算超出部分區(qū)域,判斷后返回YES;或者直接返回YES)。之后, 再重寫(xiě)hitTest:withEvent:, 因?yàn)槠淠J(rèn)返回值是包含autoCompletionTableView的container view,并不是table view, 所以在這里要轉(zhuǎn)換坐標(biāo)系,并將返回的view改為超出super view邊界的table view。按照這個(gè)步驟,超出部分的view就可以響應(yīng)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];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容