精通 UIKit UIGestureRecognizer System

这片文章深入的讲解了UIGestureRecognizer系统,在iOS11中新的手势API,以及Drag and Drap对现有的手势识别器有什么影响。

1. UITouch 和 UIGestureRecognizer

UITouch是手指触摸屏幕这一事件的代理,从触摸开始(began)到取消(cancelled)再到结束(ended),它代表了和屏幕的一个完整的交互行为。而UIGestureRecognizer是一个抽象类,可以通过设定目标(target)和动作(action)使其作用于视图(UIView)上。iOS定义了所有常见的手势,例如点击(tag),滑动(pan)或者捏合(pinch), 并且提供了相应的方法来协调处理不同手势。

为了更好的理解,我们先看来一个例子。 一个UIView,以及一个Touch Handling作为响应器(Responser)作用于这个View上。我们先来一个点击动作,当手指触摸在这个View上时,一个UITouch开始于状态began。系统会把这个状态发送给所有相关的手势识别器(UIGestureRecognizer),在本例中也就是这个点击手势识别器(UITapGestureRecognizer). 点击识别器会从初始的默认状态possible变为began, 由此触发了Touch Handling中的方法began

当手指离开View,UITouch状态变成ended,点击手势识别器状态随后也变为ended, 并且触发Touch Handling中的cancelled方法。如图1.

UIGestureRecognizer三个经常用到属性以及它们的默认值,如图2


需要注意的是,如果 var delaysTouchesBegan设置为true,那触摸响应将会被忽略。

同步识别

让我们再看另外一个例子。当同时存在两个手势识别器时,例如一个是PanGestureRecognizer另一个是TapGestureRecognizer, 如果手指在View上拖动,即使移动距离没有超过Tap手势识别器允许的最大范围,也就是说动作仍然在Tap手势所允许的范围内,然而Tap手势识别器依然会在Touch动作尚未结束前变成failed状态,从而导致Pan手势识别器被终止,如图3。

这是因为系统默认只允许一个手势识别器被执行。针对这种情况就需要用到UIGestureRecognizerDelegate中的方法:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool

这个方法可以让两个手势识别器保持同步识别从而避免上述的问题出现。需要注意的是,尽管返回true可以保证两个识别器同步识别,但返回false却不一定能阻止同步识别,因为在另外的手势识别器代理中该方法可能也被设为true,如图4。

Failure Requirements

然而,因为同步识别使得两个手势识别器都被触发,但以什么顺序发生却无法被控制,由此可能会产生一些并不是我们期待的结果。

如果我们想要一个识别器等到另一个识别器失效后再发送动作那么就需要用到失效请求(Failure Requirements). 这涉及到处理两个手势之间的关系,例如如果不希望在双击(Double Tap)的同时触发两次单击(Single Tap), 或者希望等到Tap失效后再触发Pan的动作,我们可以用到UIGestureRecognizer中一个比较静态的方法:

func require(toFail otherGestureRecognizer: UIGestureRecognizer)

或者使用UIGestureRecognizerDelegate中动态一点的两个方法:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
         shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
       shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool

从这个例子可以看出,Failure requirement的作用就是阻止另外一个识别器发送多余动作。

Hit Testing

为了确定由哪个View来响应手势,需要用到“命中测试”(Hit testing).
当触发动作发生时,命中测试使用反向深度优先搜索算法,从根视图开始不停的询问hitTest方法触摸是否在某个View中,直到找到触摸作用最深层的那个View,系统保存它并把这个触摸事件分配给这个View。这里的最深层其实是指的最后被渲染的View,我们可以把根视图window看作做最浅层,那么最深层就是离我们最近的那层。

如果你想放大或者缩小命中测试下面的两个方法通常需要被重载:

```
class UIView : NSObject {

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?

func point(inside point: CGPoint, with event: UIEvent?) -> Bool
top Created with Sketch.