Matrixzk’s Blog

keep moving

解决 UIPanGestureRecognizer 和 UIScreenEdgePanGestureRecognizer 手势的冲突问题

Jan 10th, 2015

有这样一个场景。现在想给一个 ViewController 容器增加一个手势,触发前进后退的导航功能。正常情况下,这里用 UIPanGestureRecognizer 就可以了。但是存在一个问题,如果当前界面有一个可以左右滑动的 ScrollView 时,比如是一个被双指放大了的 WebView,那么所加的这个 pan 手势就会被 ScrollView 的内部手势 (UIScrollViewPanGestureRecognizer 类型) 给屏蔽掉而不会被触发。于是这里又添加了两个 UIScreenEdgePanGestureRecognizer 边缘滑动手势,以在上述情况下通过触发边缘滑动手势进行前进后退导航。这时问题就来了,这几个手势势必会存在冲突问题 (其实真实项目中这里还有一个自定义的上下滑动手势,不过这里就先不提它了,主要说说上述两个手势的冲突问题)。下面就来解决这个问题。

首先,在 iOS 中,默认情况下在同一时刻只能有一个手势被识别,如果想改变这种默认行为,要用到 UIGestureRecognizerDelegate 协议的方法:

1
2
3
4
5
// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

当一个手势识别器对一个手势的解析将要阻塞另一个手势识别器识别其手势时,该方法将被调用。该方法默认是返回 NO。如果想让两个手势识别器同时对其各自手势进行解析,返回 YES 即可。注意,该方法返回 YES 能确保当前两个手势同时被识别,但是,返回 NO 并不能确保阻止当前两个手势被同时识别,因为另一个手势识别器的该代理方法可能会返回 YES。该方法的官方文档描述很模糊,我的理解是,在代理的所有手势在被识别之前调用该方法,两两比较确定哪些手势可以同时被触发。

针对我们的问题,有 pan 手势和边缘滑动手势两类。这里该允许哪个手势可以和其它手势同时被识别呢?都可以吗?其实只能选边缘滑动手势。因为如果这里允许 pan 手势和其他手势同时被识别的话,它就和文章开头所描述的 ScrollView 的左右滑动手势冲突,也就是两个手势会被同时识别,当左右滑动时界面被滑动的同时也触发了前进后退导航操作,根本不能愉快的浏览当前界面内容。而边缘滑动手势只有在边缘滑动时才会被触发,就不存在了上述的手势冲突问题。代码如下:

1
2
3
4
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return [gestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]];
}

接下来的问题是,允许边缘滑动手势可以和其他手势同时执行后,边缘滑动手势和左右滑动手势会同时执行,即在边缘滑动时,左右滑动手势的触发方法也会执行,由于这两个手势的触发方法基本相同,这相当于同一个触发方法被执行了两遍,肯定是不行的。

UIGestureRecognizer 有这样一个方法 requireGestureRecognizerToFail: ,比如:

1
[gestureRecognizerA requireGestureRecognizerToFail:gestureRecognizerB];

它的作用是,gestureRecognizerA 的手势在进入 Began 状态之前,需要先把该方法参数所指定的 gestureRecognizerB 的手势给 Fail 掉。当 gestureRecognizerA 在等待 gestureRecognizerB 进入 Failed 状态时,gestureRecognizerA 保持在 Possible 状态。如果 gestureRecognizerB 被 Fail 掉了,gestureRecognizerA 就开始分析当前的 touch 事件,并过渡到下一个状态。另一方面,如果 gestureRecognizerB 过渡到了 Recognized 或 Began 状态,gestureRecognizerA 就进入到 Failed 状态。上述涉及到手势的几种状态转换,详细过程参考这里 Gesture Recognizers Operate in a Finite State Machine

我的理解是,当前所有被注册代理的手势在进入 Possible 状态之后,全部被阻塞,然后两两判断之间的依赖关系,最终决定哪些手势可以被触发,或者同时被触发,进而进入 Began 状态(连续手势)或 Recognized 状态(离散手势),开始执行触发方法。这只是我的个人理解,并未在官方文档上见到详细说明。


回到我们的问题,根据上边的解释,让边缘滑动手势发送 requireGestureRecognizerToFail: 消息,左右滑动手势作为参数传进去,即在边缘滑动手势处于 Possible 状态 被触发之前,先把左右滑动手势 Fail 掉,然后边缘滑动手势进入 Began 状态,进而触发。

到目前为止,文章开头所提的问题已经解决了。

在实际的项目中因为还有动画等一些其他逻辑处理,所以还用到了 UIGestureRecognizerDelegate 的其他几个代理方法,这里大概提一下。

  • 代理方法 gestureRecognizer: shouldReceiveTouch:
1
2
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

该方法会在 touchesBegan:withEvent: 被调用之前被触发,如果返回 NO,这里的 gestureRecognizer 就看不到该 touch 事件了,也就是在这里的 gestureRecognizer 的“监控范围”内,在最接近源头的地方阻止了这个 touch 事件的传递,就像什么都没发生过。

  • 代理方法 gestureRecognizerShouldBegin
1
2
3
// called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

当一个手势将要尝试离开 UIGestureRecognizerStatePossible 进入下一个状态时,该方法被调用。这里返回 NO 会导致该手势进入 UIGestureRecognizerStateFailed 状态。默认返回 YES。注意该方法与上边那个方法的区别。上边的方法 gestureRecognizer:shouldReceiveTouch: 是在手势识别器 gestureRecognizer 开始识别手势之前被调用,即 gestureRecognizer 还未开始对手势进行分析。而这个方法被调用时,gestureRecognizer 已经开始对手势进行分析识别,且在将要尝试离开 UIGestureRecognizerStatePossible 进入下一个状态时,再给一次拦截机会。

  • 最后两个代理方法:
1
2
3
4
5
6
7
// called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
// return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
//
// note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

这两个方法和手势的 requireGestureRecognizerToFail: 方法的作用类似,但会动态决定两个手势的互斥关系。

返回顶部