iOS-hitTest:withEvent與自定義hit-testing規(guī)則

在做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。

MJ大神的圖

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


打印log的位置
觸摸view2
線索log

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

觸摸view1
屏幕快照 2015-12-03 09.52.39.png

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;
}
LLSHitTestView ![層級關(guān)系](http://upload-images.jianshu.io/upload_images/226702-c46b85b96314e86d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我們在層級比較高的view1使用自定義的hit-testing規(guī)則,其上的2、3、4,無論是否超出邊界,均能正常響應(yīng)點(diǎn)擊事件,詳見代碼。

問題

  • 為什么一次觸摸會觸發(fā)兩次hitTest:withEvent:?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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