序
在做tableView嵌套scrollView的時候怕手勢沖突,研究了一下hitTest,雖然最后沒用上,但是覺得比較有用,寫了一個DEMO,通過重寫hitTest:withEvent,實(shí)現(xiàn)了超出父視圖范圍響應(yīng)觸摸事件等自定義hit-testing規(guī)則,我的理解還很粗淺,如果有錯誤或者更優(yōu)解,歡迎大家指出,我看到后會立即修正~
DEMO
https://github.com/liulishuo/LLSHitTestView
預(yù)備知識(M了個J 、iOS developer library)
對于觸摸事件的響應(yīng),首先要找到能夠響應(yīng)該事件的對象,iOS是用hit-testing 來找到哪個視圖被觸摸了(hit-test view),也就是以keyWindow為起點(diǎn),hit-test view為終點(diǎn),逐級調(diào)用hitTest:withEvent。

- 測試用例:
在每個視圖類的hitTest:withEvent:打印兩次log:1.調(diào)用時 2.返回值時



hitTest:withEvent:調(diào)用順序:...->base->view2->view3
hitTest:withEvent:返回順序: view3(nil) -> view2(self) -> base(view2)->...


hitTest:withEvent:調(diào)用順序:...->base->view2(nil)-> base->view1
hitTest:withEvent:返回順序: view2(nil)->base, view1(self)->base(view1)->...
hitTest:withEvent:方法的處理流程:
先調(diào)用pointInside:withEvent:判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi)
1.如果返回YES,那么該視圖的所有子視圖調(diào)用hitTest:withEvent,調(diào)用順序由層級低到高(top->bottom)依次調(diào)用。
2.如果返回NO,那么hitTest:withEvent返回nil,該視圖的所有子視圖的分支全部被忽略如果某視圖的pointInside:withEvent:返回YES,并且他的所有子視圖hitTest:withEvent:都返回nil,或者該視圖沒有子視圖,那么該視圖的hitTest:withEvent:返回自己。
如果子視圖的hitTest:withEvent:返回非空對象,那么當(dāng)前視圖的hitTest:withEvent:也返回這個對象,也就是沿原路回推,最終將hit-test view傳遞給keyWindow
以下視圖的hitTest:withEvent:方法會返回nil,導(dǎo)致自身和其所有子視圖不能被hit-testing發(fā)現(xiàn),無法響應(yīng)觸摸事件:
1.隱藏(hidden=YES)的視圖
2.禁止用戶操作(userInteractionEnabled=NO)的視圖
3.alpha<0.01的視圖
4.視圖超出父視圖的區(qū)域
思路
既然系統(tǒng)通過hitTest:withEvent:做傳遞鏈取回hit-test view,那么我們可以在其中一環(huán)修改傳遞回的對象,從而改變正常的事件響應(yīng)鏈。
實(shí)現(xiàn)
強(qiáng)制指定某視圖響應(yīng)觸摸事件:
將截獲的對象替換成指定的對象,可以隨便替換,只要在替換時你能拿到要替換的對象的實(shí)例。穿透scrollView點(diǎn)擊scrollView后面的button就是這樣做的??梢栽囋嚀Q成一個(hidden=YES、userInteractionEnabled=NO、alpha<0.01)的對象,比較違反直覺,被隱藏\禁用手勢的視圖一樣能響應(yīng)觸摸事件。
經(jīng)測試,將返回的hit-test view替換為加了手勢的view,該view hidden=YES、userInteractionEnabled=NO、alpha<0.01三種情況都可響應(yīng)事件,但是如果替換為button,并且button的userInteractionEnabled=NO或者enable=NO那么無法響應(yīng)事件。忽略指定的視圖:
在hitTest:withEvent:里篩選返回值,針對指定的對象返回nil
if([view isEqual:XXX])
{
return nil;
}
這樣做的好處是不會阻斷hit-testing檢測,既可忽略指定的視圖又不會屏蔽其子視圖。
- 定制觸摸事件的響應(yīng)范圍
在hitTest:withEvent:里篩選point,判斷point在不在指定的范圍內(nèi)
if(_path)
{
if(!CGPathContainsPoint(_path.CGPath, NULL, point, NO))
{
return nil;
}
}
_path 是一段bezier曲線,詳見代碼。
- 超出父視圖范圍響應(yīng)
選定一個節(jié)點(diǎn),遍歷他的所有子節(jié)點(diǎn)用pointInside:withEvent:判斷是否命中,直到找到命中的最低層級的視圖,此時我們已經(jīng)拋棄了系統(tǒng)的hit-testing規(guī)則。
- (UIView *)getTargetView:(UIView *)view
point:(CGPoint)point
event:(UIEvent *)event
{
__block UIView *subView;
//逆序 由層級最低 也就是最上層的子視圖開始
[view.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//point 從view 轉(zhuǎn)到 obj中
CGPoint hitPoint = [obj convertPoint:point fromView:view];
// NSLog(@"%@ - %@",NSStringFromCGPoint(point),NSStringFromCGPoint(hitPoint));
if([obj pointInside:hitPoint withEvent:event])//在當(dāng)前視圖范圍內(nèi)
{
if(obj.subviews.count != 0)
{
//如果有子視圖 遞歸
subView = [self getTargetView:obj point:hitPoint event:event];
if(!subView)
{
//如果沒找到 提交當(dāng)前視圖
subView = obj;
}
}
else
{
subView = obj;
}
*stop = YES;
}
else//不在當(dāng)前視圖范圍內(nèi)
{
if(obj.subviews.count != 0)
{
//如果有子視圖 遞歸
subView = [self getTargetView:obj point:hitPoint event:event];
}
}
}];
return subView;
}

我們在層級比較高的view1使用自定義的hit-testing規(guī)則,其上的2、3、4,無論是否超出邊界,均能正常響應(yīng)點(diǎn)擊事件,詳見代碼。
問題
- 為什么一次觸摸會觸發(fā)兩次hitTest:withEvent:?