美文网首页OC基础iOS开发iOS精华
iOS-hitTest:withEvent与自定义hit-tes

iOS-hitTest:withEvent与自定义hit-tes

作者: cocoa | 来源:发表于2015-12-03 19:51 被阅读6847次

    在做tableView嵌套scrollView的时候怕手势冲突,研究了一下hitTest,虽然最后没用上,但是觉得比较有用,写了一个DEMO,通过重写hitTest:withEvent,实现了超出父视图范围响应触摸事件等自定义hit-testing规则,我的理解还很粗浅,如果有错误或者更优解,欢迎大家指出,我看到后会立即修正~

    DEMO

    https://github.com/liulishuo/LLSHitTestView

    预备知识(M了个JiOS developer library

    对于触摸事件的响应,首先要找到能够响应该事件的对象,iOS是用hit-testing 来找到哪个视图被触摸了(hit-test view),也就是以keyWindow为起点,hit-test view为终点,逐级调用hitTest:withEvent。

    MJ大神的图

    在每个视图类的hitTest:withEvent:打印两次log:1.调用时 2.返回值时


    打印log的位置 触摸view2 线索log

    hitTest:withEvent:调用顺序:...->base->view2->view3
    hitTest:withEvent:返回顺序: view3(nil) -> view2(self) -> base(view2)->...

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

    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;
    }
    
    LLSHitTestView ![层级关系](http:https://img.haomeiwen.com/i226702/c46b85b96314e86d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    我们在层级比较高的view1使用自定义的hit-testing规则,其上的2、3、4,无论是否超出边界,均能正常响应点击事件,详见代码。

    问题

    • 为什么一次触摸会触发两次hitTest:withEvent:?

    相关文章

      网友评论

      • 击水湘江:系统会根据多次HitTest做位置的修正,并且每次调用其调用栈是不一样的,所以不能在HitTest中写有副作用的代码,具体可以参考苹果官方的答复。https://lists.apple.com/archives/cocoa-dev/2014/Feb/msg00118.html
        击水湘江:@cocoa 副作用(side effects)就是对本代码块之外的其它代码产生影响。比如在这个代码块里将一个全局变量的值加一,那么就产生了副作用。如果hitTest频发被调用,那么这个副作用就将不可控,所以不应该将具有副作用的代码嵌套进去。
        cocoa:@击水湘江 谢谢指正,hitTest内副作用的代码我不太明白,如何定义这里的副作用?
      • 我是小胡胡分胡:是不是两个window导致触发两次hittest
      • moxacist:作者知道为什么会打印两次了吗 我现在还不知道,,想了好长时间了,
      • cd5e2b81487d:有个疑问,为什么第一次 触摸View2的时候,View1的 hitTest方法不执行呢?View1同样也是base的子视图啊
        cd5e2b81487d:@cocoa 大佬说的有道理:+1:
        cocoa:因为view2是后加在父视图baseview上的,hittest方法在baseview的子视图上的执行顺序是baseview.subviews这个数组的逆序,因为view2的hittest已经返回hit-test view了,所以baseview其他子视图不需要再调用hittest方法寻找hit-test view。
      • b2d1df152e2b:大神,我试了一下,这个对于父视图外的button好像不行,不知道为什么
        b2d1df152e2b:找到问题原因了,如果按照您的写法,button会返回他的子视图imageView,这个时候button就没法响应点击事件了。经过测试,在getTargetView方法最后加上这行代码就好了:
        if ([view isKindOfClass:[UIControl class]] && ![subView isKindOfClass:[UIControl class]]) {
        subView = self;
        }
        b2d1df152e2b:确切的说是 当button设置了图片的时候,那么图片区域就不能点击,非常奇怪!
      • c608:withEvent 中Event事件能识别是哪种的吗?比如是否能识别出用户是轻拍还是左右轻扫手势呢?
        天高乘云飞:同问,我这里获取不到event事件,都是空的
        _huawuque:哈喽,请问找到识别轻拍和左右轻扫手势了吗
      • 大号鱼骨头:else//不在当前视图范围内
        楼主,有个问题,既然这个地方判断到point不在该视图里,为啥还要遍历它的子视图呢?不懂
        cocoa:@文馨2526 是不是看错了,我实现的逻辑是会遍历子视图的。
        文馨2526:@cocoa 同问,我测试发现的是,如果point不在该视图内,就不遍历他的子视图了呀
        cocoa:@大号鱼骨头 因为不在父视图内不能保证不在子视图内
      • qBryant:有点晕,留着慢慢理解。。。
      • afishhhhh:我也想知道为什么一次触摸会触发两次。。。有答案了吗
        afishhhhh:@cocoa 昨天看到一篇文章说iOS7之前会调用3次,iOS7之后调用2次,具体原因不明。。。。。
        cocoa:@YyGgQq 还没有
      • zxh123456:不错
      • a3fe2d5070cf:层级由低到高 为什么是top->bottom
        cocoa:@文馨2526 你说的对,层级表示的是父子视图的树形关系。这里:“//逆序 由层级最低 也就是最上层的子视图开始” 和这里 “1.如果返回YES,那么该视图的所有子视图调用hitTest:withEvent,调用顺序由层级低到高(top->bottom)依次调用。”,说层级都不对,对同一父视图的子视图的遮盖关系,我应该用它们在subViews数组中的顺序来形容,多谢指正,我找时间改一下。
        文馨2526:@cocoa 应该不是这么理解的吧。这句话应该是说父子层级关系中最最子的view作为命中测试view。至于一个view的子view的遍历顺序,我测试看到的是,后add到view上的先被遍历。
        cocoa:@a3fe2d5070cf 屏幕最上层的视图层级最低,keyWindow层级最高。我看到文档有这么一句
        The lowest view in the view hierarchy that contains the touch point becomes the hit-test view.
      • Resory:哟。我硕儿还是高产!
        cocoa:@Resory 哟 梁总~ 请雅正
      • 曾樑:Nice吖
        cocoa:@曾樑 thx~

      本文标题:iOS-hitTest:withEvent与自定义hit-tes

      本文链接:https://www.haomeiwen.com/subject/kgwjhttx.html