一个Bug引发的对UIGestureRecognizer的思考

最近的一个项目中使用了两个功能  
* `抽屉`  
* `悬浮按钮`    

这个两个功能都跟用户的手势交互紧密相关 


抽屉  
* `滑动开关抽屉`    
* `点击开关抽屉`  

悬浮按钮  
* `拖动按钮`  
* `点击事件`  

---

##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开发者的好朋友!

posted @ 2014-03-04 14:03  Peter潘  阅读(3006)  评论(0编辑  收藏  举报