1、问题场景:
父视图上添加了一个UITabelView和一个UIButton。
在parentView上添加了UITapGestureRecognizer之后,subview中的UITableView实例无法正常响应点击事件了,但UIButton实例仍可以正常工作。
2、模拟场景现象:
图中baseView有两个subView,分别是testView和testBtn。我们在baseView和testView都重载touchsBegan:withEvent、touchsEnded:withEvent、touchsMoved:withEvent、touchsCancelled:withEvent方法,并且在baseView上添加单击手势,action名为tapAction,给testBtn绑定action名为testBtnClicked。
主要代码如下:
//baseView
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
[self.view addGestureRecognizer:tap];
...
[_testBtn addTarget:self action:@selector(testBtnClicked) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Cancelled");
}
- (void)tapAction {
NSLog(@"=========> single Tapped");
}
- (void)testBtnClicked {
NSLog(@"=========> click testbtn");
}
//test view
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Cancelled");
}
情景A:单击baseView,输出结果为:
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Cancelled
情景B:单击testView,输出结果为:
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Cancelled
情景C:单击testBtn,输出结果为:
=========> click testbtn
情景D:按住testView,过5秒后或更久释放,输出结果为:
=========> test view touchs Began
=========> test view touchs Ended
3、分析:
1、情景A和B
情景A和B,都是在单击之后,既响应了手势的tap事件,也让响应链方法执行了。为什么两个响应都执行了呢?
手势识别器获得识别触摸的第一个机会。
一个窗口延迟将触摸对象传递到视图,使得手势识别器可以首先分析触摸。在延迟期间,如果手势识别器识别出触摸手势,则窗口不会将触摸对象传递到视图,并且还将先前发送到作为识别的序列的一部分的视图的任何触摸对象取消。
触摸事件首先传递到手势上,如果手势识别成功,就会取消事件的继续传递,否则,事件还是会被响应链处理。具体地,系统维持了与响应链关联的所有手势,事件首先发给这些手势,然后再发给响应链。
这样可以解释情景A和B了。
首先,我们的单击事件,是有有手势识别这个大哥来优先获取。只不过,手势识别是需要一点时间的。在手势还是Possible状态的时候,事件传递给了响应链的第一个响应对象(baseView或者testView)。
这样自然就去调用了,响应链UIResponder的touchsBegan:withEvent方法,之后手势识别成功了,就会去cancel之前传递到的所有响应对象,于是就会调用它们的touchsCancelled:withEvent:方法。
2、情境C
好了,情景A和B都可以解释明白了。但是,请注意,按这样的解释为什么情景C没有触发响应链的方法呢?
这里可以说是事件响应的一个特例。
在iOS 6.0及更高版本中,默认控制操作可防止重叠的手势识别器行为。例如,按钮的默认操作是单击。如果您有一个单击手势识别器附加到按钮的父视图,并且用户点击按钮,则按钮的动作方法接收触摸事件而不是手势识别器。这仅适用于与控件的默认操作重叠的手势识别,其中包括:单个手指单击UIButton,UISwitch,UISegmentedControl,UIStepper和UIPageControl.
单个手指在UISlider的旋钮上滑动,在平行于滑块的方向上。在UISwitch的旋钮上的单个手指平移手势与开关平行的方向。
所以呢,在情境C,里面testBtn的默认action,获取了事件响应,不会把事件传递给父视图baseView,自然就不会触发,baseView的tap事件了。
3、情境D
在情景D中,由于长按住testView不释放,tap手势就会识别失败,因为长按就已经不是单击事件了。手势识别失败之后,就可以继续正常传递给testView处理。
所以,只有响应链的方法触发了。
4、实际开发遇到的问题解决
基本的开发目标,不让父视图的手势识别干扰子视图UIView的点击事件响应或者说响应链的正常传递。
一般都会是重写UIGestureRecognizerDelegate中的- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch方法。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
// 若为UITableViewCellContentView(即点击了tableViewCell),
if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {
// cell不需要响应父视图的手势,保证didselect可以正常
return NO;
}
//默认都需要响应
return YES;
}
4、总结
- 手势响应是大哥,点击事件响应链是小弟。单击手势优先于UIView的事件响应。大部分冲突,都是因为优先级没有搞清楚。
- 单击事件优先传递给手势响应大哥,如果大哥识别成功,就会直接取消事件的响应链传递。如果大哥识别失败了,触摸事件会继续走传递链,传递给响应链小弟处理。
- 手势识别是需要时间的。
手势识别有一个状态机的变化。在possible状态的时候,单击事件也可能已经传递给响应链小弟了。