iOS-hitTest:withEvent与自定义hit-testing规则
2016-01-08 10:27
736 查看
序
在做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。
MJ大神的图测试用例:在每个视图类的hitTest:withEvent:打印两次log:1.调用时 2.返回值时
打印log的位置
触摸view2
线索loghitTest:withEvent:调用顺序:...->base->view2->view3
hitTest:withEvent:返回顺序: view3(nil) -> view2(self) -> base(view2)->...
触摸view1
屏幕快照 2015-12-03 09.52.39.pnghitTest: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
定制触摸事件的响应范围
在hitTest:withEvent:里筛选point,判断point在不在指定的范围内
超出父视图范围响应
选定一个节点,遍历他的所有子节点用pointInside:withEvent:判断是否命中,直到找到命中的最低层级的视图,此时我们已经抛弃了系统的hit-testing规则。
LLSHitTestView
层级关系我们在层级比较高的view1使用自定义的hit-testing规则,其上的2、3、4,无论是否超出边界,均能正常响应点击事件,详见代码。
问题:为什么一次触摸会触发两次hitTest:withEvent:?
在做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。
MJ大神的图测试用例:在每个视图类的hitTest:withEvent:打印两次log:1.调用时 2.返回值时
打印log的位置
触摸view2
线索loghitTest:withEvent:调用顺序:...->base->view2->view3
hitTest:withEvent:返回顺序: view3(nil) -> view2(self) -> base(view2)->...
触摸view1
屏幕快照 2015-12-03 09.52.39.pnghitTest: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
层级关系我们在层级比较高的view1使用自定义的hit-testing规则,其上的2、3、4,无论是否超出边界,均能正常响应点击事件,详见代码。
问题:为什么一次触摸会触发两次hitTest:withEvent:?
相关文章推荐
- iOS 打印日志显示系统详细时间,类名,行号及打印值
- ARC 完全指南
- iOS NSNotificationCenter详解
- ios9 简单粗暴总结
- iOS图片拉伸技巧—— resizableImageWithCapInsets
- iOS监听键盘弹出
- iOS使用NSUserDefaults保存用户名和密码
- ios non-arc to arc 之三
- iOS cell排序
- 学习笔记5: 仿ios Dialog
- xcode7、iOS9 设置启动图片(Launch Image)
- IOS 去空格处理 特殊字符处理
- ios 非arc转arc 之二
- IOS笔记
- iOS开发调试技巧总结(持续更新中)
- iOS倒计时的实现
- ios 关于非arc 工程转arc
- iOS用CGContextRef画图(文字、圆、直线、弧线、矩形、扇形、椭圆、三角形、圆角矩形、贝塞尔曲线、图片)
- iOS自定义tableViewCell中的按钮无法点击的解决方法
- iOS屏幕横竖屏旋转相关