iOS-hitTest:withEvent与自定义hit-testing规则

在做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://upload-images.jianshu.io/upload_images/226702-c46b85b96314e86d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

问题

  • 为什么一次触摸会触发两次hitTest:withEvent:?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 56,567评论 51 597
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,987评论 4 26
  • 用户以多种方式操纵他们的iOS设备,例如触摸屏幕或摇动设备。 iOS会解释用户何时以及如何操作硬件并将此信息传递到...
    坤坤同学阅读 3,975评论 7 19
  • 前言: 按照时间顺序,事件的生命周期是这样的:事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的vie...
    VV木公子阅读 138,768评论 194 1,202
  • 前言:按照时间顺序,事件的生命周期是这样的:事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view...
    贝勒老爷阅读 998评论 1 9