最近的一个项目中使用了两个功能 * `抽屉` * `悬浮按钮` 这个两个功能都跟用户的手势交互紧密相关 抽屉 * `滑动开关抽屉` * `点击开关抽屉` 悬浮按钮 * `拖动按钮` * `点击事件` --- ##BUG 这两个功能都较为普遍,所以我和同事一人在网上找了一个相关的demo来完成。 不过最后这两个功能出现了`冲突`: > 在拖动悬浮按钮的时候,抽屉的功能也被触发,造成两者都不能顺畅执行,两者同时滑动一下之后,按钮就停止运动,而抽屉继续完成剩余行为。 > 这个结果和原本预想的不一样,原本的预计是,当用户交互发生在按钮的时候,抽屉的手势不应该被触动,只有在交互发生在悬浮按钮之外的时候,抽屉的手势才会被触动。 可见,在抽屉和悬浮按钮上,`触摸`有一定的`冲突`。 --- ##BUG原因 我查看了两个demo中对手势的完成方式。 抽屉中使用了`UIPanGestureRecognizer` 悬浮按钮通过重写了`Touch-Event Handling Methods` * touchesBegan:withEvent: * touchesMoved:withEvent: * touchesEnded:withEvent: * touchesCancelled:withEvent: 通过设置log值,我观察了一下问题产生时候程序调用的流程。 ``` 2014-02-26 11:32:03.895 Gesture[1486:60b] button touch began 2014-02-26 11:32:03.989 Gesture[1486:60b] button touch moved 2014-02-26 11:32:04.005 Gesture[1486:60b] view panAction 2014-02-26 11:32:04.008 Gesture[1486:60b] button touch cancelled 2014-02-26 11:32:04.021 Gesture[1486:60b] view panAction 2014-02-26 11:32:04.023 Gesture[1486:60b] view panAction ``` 发现当悬浮按钮的`touch began` 和 `touch moved`方法调用后,抽屉的手势起了作用,同时原本悬浮按钮的`touch cancel`被触发。接下来只执行抽屉的手势。 这就是为什么会发生我之前描述的问题的原因。 对于这一点[Apple文档](https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html#//apple_ref/occ/instp/UIGestureRecognizer/cancelsTouchesInView)描述的相当清楚 > A gesture recognizer operates on touches hit-tested to a specific view and all of that view’s subviews. > ...... > cancelsTouchesInView—If a gesture recognizer recognizes its gesture, it unbinds the remaining touches of that gesture from their view (so the window won’t deliver them). The window cancels the previously delivered touches with a (touchesCancelled:withEvent:) message. If a gesture recognizer doesn’t recognize its gesture, the view receives all touches in the multi-touch sequence. 当手势添加到view上的时候,手势会开始观察view和view上的subviews。 当手势被识别的时候,之前的touch将被取消同时不会再传递 当然这个可以通过设置cancelsTouchesInView为NO来取消或者开启,具体的可以看看[Apple文档](https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html#//apple_ref/occ/instp/UIGestureRecognizer/cancelsTouchesInView) --- ##疑问 在我原本的印象当中,UIGestureRecognizer是对Touch-Event Handling Methods的高一层封装,就算两者同时使用,本质依旧相同,按理说应该不会发生这些问题。当悬浮按钮截获了交互之后,就不应该继续传到superview上去,也就不应该触发抽屉的手势。 但是最终结果是两者都触发了。 现在一想,发现自己对UIGestureRecognizer的第一印象是错误的。 UIGestureRecognizer的UITouch获得和普通的Responder的UITouch传递还不太一样。 所以我花了点时间好好看了一下苹果的文档[Event Handling Guide for iOS](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW44) ``` 以下是我看文档前两个疑惑: 1. UITouch是如何传递的 2. 父视图的UIGestureRecognizer是如何早于子视图获取到UITouch的 ``` --- ## Gesture Recognizer对UITouch的影响 我们和手机交互的主要方式是通过屏幕,我们的一些手势也都是在屏幕上操作,所以我们最早的触摸当然是被电容屏所接收到的。当然这个回答不是我们想要的答案。我们所关心的是在电容屏所接收到的触摸在系统里的处理。 那么在电容屏接收到触摸后,这些触摸首先去了哪里呢? Apple文档对于这个问题给出了一副图来说明: ![图1](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/path_of_touches_2x.png) 1. 用户的手势交互以UITouch和UIEvent的形式存储在当前应用程序的事件队列中。 2. UIApplication单例将对象将事件从队列的顶部取出,然后派发下去到焦点窗口即拥有当前用户事件焦点的窗口(当前应用程序窗口UIWindow) 3. UIWindow在把touch传递给view上的手势识别器。 4. 如果手势识别器识别成功,不再传递给view,识别失败则传递给view 描述概念总是头疼的,简单来打个比方,UIWindow就是一个后妈,手势识别器就是后妈的亲儿子,view呢是不是亲儿子,后妈每次拿到好吃的都是先分给亲儿子先,如果亲儿子喜欢吃,就不会再给另外一个儿子吃了,不仅不让吃,还会把之前的给的全拿回来,只有亲儿子不吃的东西才会丢给另外一个儿子吃。 这里面涉及了UIGestureRecognizer和Event Handle Method的方法调用,再来一张图配合理解。 ![图2](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/recognize_touch_2x.png) 扯了个蛋,自己也理解的通透了,挺好挺好。 ##后记 这篇博客主要记录了我对于bug发生原因的记录,不过在查询文档过程中,还看到了很多其他相关的内容,不过鉴于时间有限,也没法一下子记录下来。等空下来的时候,我会自己再总结一下响应链方面的机制。最后不得不说,苹果文档真是IOS开发者的好朋友!