今天,群里有人问了这个问题:添加了touch事件之后怎么阻止touch事件传递到子view。其实看了官方的文档Event Handling Guide for iOS的童鞋,应该是没有问题的。但是自己还是总结一下。
触摸之后,主要的步骤如下:
(1), 事件分发:如何确定当前点击的点由哪个view来处理?
hit-test来确定hit-view
(2), 事件响应:确定hit-view之后,如何处理事件?
当确定了hit-view之后,第一响应者就是当前的hit-view,然后就会根据响应者链来处理触摸事件。
有手势的先处理手势,手势识别失败后,执行touch系列回调处理。
情景应用
问题1:如果父视图userInteractionEnable是NO,这时候子视图能接收touch事件吗?
分析:不能
因为在hit-testing的时候父视图返回nil了,那么就轮不到子视图来hit-testing了。
这也是为何在imgView上面加载UIButton的时候,button无法响应的原因
问题2:如果一个视图A(A上面加载了手势处理)被视图B盖住了,A与B都是视图X的子视图,那么怎样让A的手势能响应?
分析: 因为B盖住了A,所以hit-test的结果之后,hit-view肯定是B,A的手势无法响应,
可以这么做:
1, 设置B.userInteractionEnable = NO;
2, B.hidden = YES;
3, B.alpha = 0;
上面的3种情况下,A都可以响应手势了。
因为这么设置之后,在hit-testing的时候,B视图的hitTest方法返回的是nil,最终的hit-view是A,所以触摸事件就轮到了A来处理。
问题3:如果一个viewA不希望它的subView来处理touch事件,而是由自己处理,怎么办?
分析:
viewA不希望触摸事件传递到它的subView, 也就是viewA自己阻断触摸事件的传递,只要让触摸后最终的hit-view是他自己就可以了。
比如viewA的subView为YLViewSub1
有如下2种方法:
方法一:不推荐
在viewA的.m文件中重载hitTest(注意:viewA是一个自定义的UIView才能重载此方法),如下
-(
UIView
*)hitTest:(
CGPoint
)point withEvent:(
UIEvent
*)event
{
UIView *hitView = [super hitTest:point withEvent:event];
// 此时hitView是已经检测出的hit-view了,是self or subViews(hitted subView)
/*
注意:
*
如果想要阻断触摸事件传递给
subView
,下面的
2
种做法是不太合理的:
*/
/* 方法1:
*
不管
3721
的直接返回
self
也是不对的,因为当没有点击在
self
上
(
包括它的
subView)
的时候,
self
都成了
hit-view
*/
return
self;
/* 方法2:
*
因为
hitView
可能返回的是它的
subView
的
subView
的
subView...
*
所以不能这么做
.
如果能确定
self
的
subView
只有一级,这么做也是可以的
.
*/
if
(hitView.
superview
==
self
&& hitView ==
self
) {
return
self; // 点击在它的subView上也由它自己来处理,subView永远不是hit-view(永远不会是第一响应者,不处理触摸)
}
/* 正确的做法:也就是下面的方法二,在subView中重载hitTest
* 1,
可以在
self
的
subView
中重载
hitTest
方法,直接返回
nil,
那么点击在
self
的
subView
上的时候,最后
hit-view
还是
self
*
所以在重载此方法的时候一定要搞清楚具体的应用场景
.
*/
return
nil;
}
可以看到直接在viewA中重载还是不太好的实现,而且如果viewA是一个vc.view,那么就没办法重载hitTest方法了。
方法二:推荐
在viewA的subView(
YLViewSub1
)的类中重载hitTest,
在YLViewSub1的.m文件中,
-(
id
)hitTest:(
CGPoint
)point withEvent:(
UIEvent
*)event
{
UIView
*hitView = [
super
hitTest:point withEvent:event];
if
(hitView ==
self
) {
return
nil;
} else {
return hitView;
}
}
有人可能有疑问了:上面说过直接在这里返回nil不就可以了,为什么还要分情况处理。
其实这要看具体的情况了,如果
YLViewSub1上面还有subView,直接的返回nil,那么就会忽略掉,所以如果你想全部忽略掉就直接返回nil,不然可以像上面这么处理。
另外,还有一种更简单的做法,直接让viewA的subView的userInteraction为NO,那么subView就不会受到触摸消息了。
扩展:
hit-test还有另外一个场景,比如viewA有B,C两个subView,但是他们2个有重合的部分,点击重合部分,那如何指定让B响应还是C来响应。
分析:
默认情况下,hit-test的时候,是从subViews的最顶上的subView开始执行hit-test,即假如先加B,再加载C,那么hit-test就是先C先B后,这样点击重合部分,那就是C就是hit-view,那么由C来响应。
如何变成让B先响应呢?
-(
UIView
*)hitTest:(
CGPoint
)point withEvent:(
UIEvent
*)event
{
UIView
*hitView = [
super
hitTest:point withEvent:event];
if (hitView == viewC) { // 点击在C上的时候,让hit-view为B,那么B就是第一响应者
return
viewB;
}
else
{
return
hitView;
}
}
这样,当点击重合部分就由B响应,点击B,C非重合部分由他们各自响应.
问题4:如果一个view自己不愿意处理touch事件,但是希望它的subViews处理,怎么办?
应用场景:这个问题有点sb,因为默认情况下就是subView来先处理,应用场景在哪?
可能是如果点击view自己,让他的父视图处理,点击view上面的subView由subView响应更合理?
分析:
设置view.userInteractionEnable = NO;之后,虽然自己不会响应touch事件,但是它的子view也不会响应了,
所以不能这么做。这时候就需要使用hitTest来处理,
-(
UIView
*
)hitTest:(
CGPoint
)point withEvent:(
UIEvent
*)event {
UIView
*hitView = [
super
hitTest
:point
withEvent
:event];
if
(hitView ==
self
) {
return nil; // 是self的时候, 不做处理,由它的父视图处理
}
else
{
return
hitView;
//
是
subView
的时候
,
由
subView
去处理
}
}
问题5:如果一个view自己不想处理,也不愿意往它的响应者链传递让别人处理,怎么办?
分析:首先,确定处理对象的时候必须是自己,然后,在自己这里处理的时候丢弃,
也就是自己重载响应函数,然后响应函数里面不做任何事,这样就不会继续向上传递了,也就是在自己这里做一个空处理来截止响应的处理.
问题6:一个全屏的UIView上加载了tap手势,在此view上加载一个UITableView,点击cell的时候没有执行tableview的didSelected:方法,而是响应了_onTap手势,如何处理?
解决方法:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
// tianyi memo:
// 点击在tableView上时,因为tableView自己不响应tap,所以会交给它的父视图self来响应,也就是响应_onTap:,但这不是我们想要的
// 我们需要点击tableView上面时,响应tableView的didSelectRowAtIndexPath方法.点击其他空白地方相应_onTap:
// 返回NO表示,tap手势不会根据响应者链传递了,当前的touch对象会被忽略,也就是丢弃这个手势,
// 丢弃手势之后,相当于手势识别失败,然后就会走默认的touch系列回调方法,我猜测在这个时候UITableView执行了自己默认的选择cell的流程.
if ([touch.viewisDescendantOfView:_tableView]) {
returnNO;
}
returnYES;
}
如果有不正确或者考虑不完善的地方,欢迎指正交流。