序
在做tableView嵌套scrollView的時候怕手勢沖突,研究了一下hitTest,雖然最后沒用上,但是覺得比較有用,寫了一個DEMO,通過重寫hitTest:withEvent,實現了超出父視圖范圍響應觸摸事件等自定義hit-testing規則,我的理解還很粗淺,如果有錯誤或者更優解,歡迎大家指出,我看到后會立即修正~
DEMO
https://github.com/liulishuo/LLSHitTestView
預備知識(M了個J 、iOS developer library)
對于觸摸事件的響應,首先要找到能夠響應該事件的對象,iOS是用hit-testing 來找到哪個視圖被觸摸了(hit-test view),也就是以keyWindow為起點,hit-test view為終點,逐級調用hitTest:withEvent。
- 測試用例:
在每個視圖類的hitTest:withEvent:打印兩次log:1.調用時 2.返回值時
hitTest:withEvent:調用順序:...->base->view2->view3
hitTest:withEvent:返回順序: view3(nil) -> view2(self) -> base(view2)->...
hitTest:withEvent:調用順序:...->base->view2(nil)-> base->view1
hitTest:withEvent:返回順序: view2(nil)->base, view1(self)->base(view1)->...
hitTest:withEvent:方法的處理流程:
先調用pointInside:withEvent:判斷觸摸點是否在當前視圖內
1.如果返回YES,那么該視圖的所有子視圖調用hitTest:withEvent,調用順序由層級低到高(top->bottom)依次調用。
2.如果返回NO,那么hitTest:withEvent返回nil,該視圖的所有子視圖的分支全部被忽略如果某視圖的pointInside:withEvent:返回YES,并且他的所有子視圖hitTest:withEvent:都返回nil,或者該視圖沒有子視圖,那么該視圖的hitTest:withEvent:返回自己。
如果子視圖的hitTest:withEvent:返回非空對象,那么當前視圖的hitTest:withEvent:也返回這個對象,也就是沿原路回推,最終將hit-test view傳遞給keyWindow
以下視圖的hitTest:withEvent:方法會返回nil,導致自身和其所有子視圖不能被hit-testing發現,無法響應觸摸事件:
1.隱藏(hidden=YES)的視圖
2.禁止用戶操作(userInteractionEnabled=NO)的視圖
3.alpha<0.01的視圖
4.視圖超出父視圖的區域
思路
既然系統通過hitTest:withEvent:做傳遞鏈取回hit-test view,那么我們可以在其中一環修改傳遞回的對象,從而改變正常的事件響應鏈。
實現
強制指定某視圖響應觸摸事件:
將截獲的對象替換成指定的對象,可以隨便替換,只要在替換時你能拿到要替換的對象的實例。穿透scrollView點擊scrollView后面的button就是這樣做的。可以試試換成一個(hidden=YES、userInteractionEnabled=NO、alpha<0.01)的對象,比較違反直覺,被隱藏\禁用手勢的視圖一樣能響應觸摸事件。
經測試,將返回的hit-test view替換為加了手勢的view,該view hidden=YES、userInteractionEnabled=NO、alpha<0.01三種情況都可響應事件,但是如果替換為button,并且button的userInteractionEnabled=NO或者enable=NO那么無法響應事件。忽略指定的視圖:
在hitTest:withEvent:里篩選返回值,針對指定的對象返回nil
if([view isEqual:XXX])
{
return nil;
}
這樣做的好處是不會阻斷hit-testing檢測,既可忽略指定的視圖又不會屏蔽其子視圖。
- 定制觸摸事件的響應范圍
在hitTest:withEvent:里篩選point,判斷point在不在指定的范圍內
if(_path)
{
if(!CGPathContainsPoint(_path.CGPath, NULL, point, NO))
{
return nil;
}
}
_path 是一段bezier曲線,詳見代碼。
- 超出父視圖范圍響應
選定一個節點,遍歷他的所有子節點用pointInside:withEvent:判斷是否命中,直到找到命中的最低層級的視圖,此時我們已經拋棄了系統的hit-testing規則。
- (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 轉到 obj中
CGPoint hitPoint = [obj convertPoint:point fromView:view];
// NSLog(@"%@ - %@",NSStringFromCGPoint(point),NSStringFromCGPoint(hitPoint));
if([obj pointInside:hitPoint withEvent:event])//在當前視圖范圍內
{
if(obj.subviews.count != 0)
{
//如果有子視圖 遞歸
subView = [self getTargetView:obj point:hitPoint event:event];
if(!subView)
{
//如果沒找到 提交當前視圖
subView = obj;
}
}
else
{
subView = obj;
}
*stop = YES;
}
else//不在當前視圖范圍內
{
if(obj.subviews.count != 0)
{
//如果有子視圖 遞歸
subView = [self getTargetView:obj point:hitPoint event:event];
}
}
}];
return subView;
}
我們在層級比較高的view1使用自定義的hit-testing規則,其上的2、3、4,無論是否超出邊界,均能正常響應點擊事件,詳見代碼。
問題
- 為什么一次觸摸會觸發兩次hitTest:withEvent:?