在不考虑内部实现机制的情况下,我们使用三种方式来处理IOS手势:
1. Gesture Recongnizers — UIGestureRecognizer 及其子类
2. touches 响应 — touchesBegan、touchesEnd..等
3. Target-Action 机制 — UIControl及其子类
本文探讨了这几种处理手势事件的混合使用可能会产生的冲突情况,并提供了解决方法,希望看过这篇文章的朋友们不再踩这些坑。
想象这样一个场景,我们自定义了一个View,使用UIView作为其父类,通常我们只是将其本身作为一个承载子view的容器,但是很多情况下我们不得不对其进行拓展使其能够响应用户手势(例如点击跳转),有3个方案可选。
1. 绑定UIGesture Recognizer ,Apple推荐做法
2. 实现UIVIew的touch事件touchesEnded:withEvent
* 如果父视图响应touchesBegan:withEvent 事件的话,见第二节 Touch Events 冲突
* 如果父视图响应UIGesture Recongnizer 事件的话,见第三节 Touch Events 冲突
3. 将父类从UIView改成UIControl
* 如果父视图响应UIGesture Recongnizer ,见第四节 UIGestureRecognizer和UIControl的冲突
在这个例子中,我们让父view响应touchesBegan事件,子view(背景红色,占据父view的上半部分)的touchesEnded处理跳转事件
* 当点击下半部分区域时,父view背景改变
* 当点击上半部分区域时,弹出跳转提示(期望结果),同时,父view背景改变(非期望结果)
除了使用UIGestureRecognizer代替touchesEnded之外,还可以通过实现子view所有的touches事件来防止touches event 被响应链传递。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 防止此事件被响应链传递
}
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer.
为了搞清楚这两者同时存在时响应的时序问题,我们在view上绑定tapGestureRecognizer的同时实现所有的touches事件,尝试以下操作并观察结果
2016-10-30 15:37:47.711 LOG:::VCTouchesVSGestures___view:touchesBegan:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:37:47.716 VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:37:47.717 LOG:::VCTouchesVSGestures___view:touchesCancelled:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:44:16.222 LOG:::VCTouchesVSGestures___view:touchesBegan:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:44:16.236 LOG:::VCTouchesVSGestures___view:touchesMoved:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
2016-10-30 15:44:16.267 LOG:::VCTouchesVSGestures___view:touchesMoved:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
....
....
2016-10-30 15:44:18.137 LOG:::VCTouchesVSGestures___view:touchesEnded:withEvent:___<VCTouchesVSGestures: 0x7f9183ea8510>
UIGestureRecognizer类提供了三个属性来处理这两者之间的关系,它们分别是:
cancelsTouchesInView
默认为YES,当手势识别成功时,UIApplication将发送touchesCancel消息。
设置为NO时,即使手势识别成功,UIView仍然会正常响应touches事件
delaysTouchesBegan
默认为NO,如果为YES,只要gestureRecognizer没有识别失败(识别中or识别成功),那么view永远接受不到touchesbegan。在识别失败的情况下,gesture绑定的view将会被触发touchesBegan(可能跟着一系列的touchesMove)
delaysTouchesEnded
默认为YES,其效果类似delaysTouchesBegan
有兴趣的读者可以将上述例子中的gesture的上述三个属性修改一下,查看效果
点击程序的任意区域,都没有跳转提示,只有父view的背景不断变化,似乎子view添加的手势处理事件完全没有被触发。
经过调试发现,完全没有响应子view的UIControl的touchUpInside事件,而是响应了父控件的tap事件。是因为UIGesture Recognizer的级别较高,而且由于cancelsTouchesInView默认是yes,所以UIControl不足以识别出touchUpInside事件,假如我们将UIControl的响应事件改成touchDown的话,你会发现,两者都响应。同样,保留touchUpInside将cancelsTouchesInView设置成NO,有同样的效果。
实际上,UIGestureRecognizer和UIControl的冲突本质上是UIGestureRecognizer和Touches响应的冲突,可参考上一节。
但是,并不是UIControl的所有子类都会遭遇同样的问题。假如我们把UIControl换成UIButton的话,只会响应UIButton的touchUpInside,不会响应父View的tap事件。原因见苹果官方说明:
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
* A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
* A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
* A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.