从 WebKit 源码中学习手势处理的高级用法——WKWebView中的手势识别

5,196 阅读11分钟

更多精彩文章,欢迎关注作者微信公众号:码工笔记

一、iOS 系统中手势处理的背景知识

在 iOS 系统中,屏幕点击事件从开始到结束主要经历以下步骤[1]:

  1. 用户点击屏幕,生成一个硬件触摸事件

  2. 操作系统将硬件触摸事件包装成 IOHIDEvent 后发送给 Springboard,Springboard 再将 IOHIDEvent 发给当前打开的 App 进程

  3. App 进程收到事件后,将主线程 Runloop 唤醒(Source1),Source1 回调中触发 Source0 回调,将 IOHIDEvent 包装成 UIEvent 对象,发送给顶层的 UIWindow(-[UIApplication sendEvent:])

  4. UIWindow 对 UIEvent 中的每个 UITouch 实例调用 hitTest 方法寻找其 hitTestView,并在 hitTest 递归调用过程中,记录最终的 hitTestView 及其各父视图上挂载的 gestureRecognizer(记录为gestureRecognizers属性)

    • 同级的多个 view 之间 hitTest 调用顺序为逆序(后 addSubView 的先调用 hitTest)

    • 默认根据 pointInside 方法的返回值来决定是否递归查找其子 view

  5. 将每个 UITouch 对象发给其对应的 gestureRecognizers 对象以及 hitTestView(即调用它们的 touchesBegin 方法)

    • 识别成功的 gestureRecognizer 将独占相关的 touch,所有其他 gestureRecognizer 和 hitTestView 都将收到 touchsCancelled 回调,并且以后不会再收到此 touch 的事件

      • 一个特例是:系统默认的 UIControl(UISlider, UISwitch 等)的控件的父view上的 gestureRecognizer 优先级低于 UIControl 本身

      • 也需要配合相关 gestureRecognizer 冲突解决相关方法(如 canBePreventedByGestureRecognizer:、canPreventGestureRecognizer:等)的具体实现使用

    • 如果 hitTestView 实例不响应 touchesBegin 等方法,则顺着 responder chain 继续找 nextResponder 来调用

    • 若实现了 touchesBegin 等方法,则在其中调用 [super touchesBegin] 方法会将 touch 事件沿着 responder chain 向上传递

二、WebKit 中的应用

1. 用 hitTest 扩展触摸手势的响应区域或范围

通过重载 -hitTest:withEvent: 方法并在其中添加对非当前 View 子 View 的 hitTest 调用,即可扩展点击响应的区域或范围。实现中注意:

  • 在手动调用 hitTest:withEvent: 方法时,需要将坐标 point 转到目标 View 的坐标系内

  • 调用 [super hitTest:withEvent:] 来递归 hitTest 其子View(默认逻辑)

//WKContentView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //这里 _interactionViewsContainerView 在视图层级中与WKContentView是同级的(同是WKWebView的子View),在这里对其子 view 调用 hitTest 方法,可以达到以扩展点击区域的效果
    for (UIView *subView in [_interactionViewsContainerView.get() subviews]) {
        UIView *hitView = [subView hitTest:[subView convertPoint:point fromView:self] withEvent:event];
        if (hitView) {
            return hitView;
        }
    }
    
    ...
    //默认的 hitTest 逻辑,递归遍历子View
    UIView* hitView = [super hitTest:point withEvent:event];
    
    ...
    
    return hitView;
}

2. 用 hitTest 限制触摸手势的响应区域或范围

通过重载 hitTest:withEvent: 方法,并在其中定义要对具体哪些 View 做 hitTest 或直接返回。

  • 注意以下例程中未调用 [super hitTest:withEvent:],即不走遍历子 View 做 hitTest 的默认逻辑

详见代码中的注释:

//WKCompositingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //获取 hitTestView
    return [self _web_findDescendantViewAtPoint:point withEvent:event];
}

//其具体实现为:

//UIView(WKHitTesting)
- (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event
{
    Vector<UIView *, 16> viewsAtPoint;
    
    //只收集符合位置等条件的非 WKCompositingView 类型的 view, 存到 viewsAtPoint 中
    WebKit::collectDescendantViewsAtPoint(viewsAtPoint, self, point, event);

    ...

    //对这些收集的 view 再根据业务逻辑进行过滤,根据不同类别找到其目标View
    for (auto *view : WTF::makeReversedRange(viewsAtPoint)) {
        //对此类 view 做递归 hitTest
        if ([view conformsToProtocol:@protocol(WKNativelyInteractible)]) {
            //natively interactible
            CGPoint subviewPoint = [view convertPoint:point fromView:self];
            return [view hitTest:subviewPoint withEvent:event];
        }
        
        //对此类 view 直接返回本身,不递归查找子 view
        if ([view isKindOfClass:[WKChildScrollView class]]) {
            if (WebKit::isScrolledBy((WKChildScrollView *)view, viewsAtPoint.last())) {
                //child scroll view
                return view;
            }
        }
        //同上
        if ([view isKindOfClass:WebKit::scrollViewScrollIndicatorClass()] && [view.superview isKindOfClass:WKChildScrollView.class]) {
            if (WebKit::isScrolledBy((WKChildScrollView *)view.superview, viewsAtPoint.last())) {
                //scroll indicator of child scroll view
                return view;
            }
        }
        //ignoring other views
    }
    return nil;
}

3. 用手势冲突解决机制来实现对 CSS touch-action 定义的手势生效规则的支持

  • CSS 中的 touch-action 属性用于设置触摸屏用户如何操纵元素的区域,主要有以下取值(详见[2]):

      /* Keyword values */
      touch-action: auto;
      touch-action: none;
      touch-action: pan-x;
      touch-action: pan-left;
      touch-action: pan-right;
      touch-action: pan-y;
      touch-action: pan-up;
      touch-action: pan-down;
      touch-action: pinch-zoom;
      touch-action: manipulation;
    
      /* Global values */
      touch-action: inherit;
      touch-action: initial;
      touch-action: unset;
    

    举例来说,如果某个 DOM 元素的 touch-action 属性设置为 none 时,WebView 是不允许对此元素使用触摸手势进行滑动的。

  • 实现方案:

    • 为了实现对 touch-action 的支持,WebKit 中定义了一个特殊的 WKTouchActionGestureRecognizer,并将其作为最后一个 gestureRecognizer 添加到了 WKContentView 上(参考第一部分提到的事件派发优先级,最后一个 gestureRecognizer 优先级最高)

    • WKTouchActionGestureRecognizer 的 touchesBegin/touchesMoved/touchesEnded 方法实现中,都是直接调用了 _updateState 将手势识别状态置为识别成功。这样根据第一部分中讲的手势冲突的处理逻辑,其他 gestureRecognizer 和 hitTestView 就会收到 touchesCancelled 回调——从而达到阻止其他手势响应的效果。

      • 注意:并不是阻止了所有手势,具体哪些可以不阻止,还取决于下面要讲的几个解决手势冲突的方法实现
    • WKTouchActionGestureRecognizer 的 canBePreventedByGestureRecognizer: 方法返回了 NO,代表即使别的 gestureRecognizer 已经识别为成功,它也仍旧可以识别成功并继续收到touches消息

    • WKTouchActionGestureRecognizer 的 canPreventGestureRecognizer: 方法,根据 touch-action 的设置,放行或禁止 WKWebView 中预定义的几种手势

      //WKTouchActionGestureRecognizer  (作为最后一个 gestureRecognizer 加在 WKContentView 上)
    
      //touchBegin 中即设置 recognized 状态,可以使别的 gestureRecognizer 手势识别失败(cancelled)
    
      - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          [self _updateState];
      }
    
      - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          [self _updateState];
      }
    
      - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          [self _updateState];
      }
    
      - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          [self _updateState];
      }
    
      //此方法设置gestureRecognizer为识别成功状态,这样就可以使其他gestureRecognizer或hitTestView停止接收事件(配合其他手势冲突解决的方法)
      - (void)_updateState
      {
          // We always want to be in a recognized state so that we may always prevent another gesture recognizer.
          [self setState:UIGestureRecognizerStateRecognized];
      }
    
      //即使是其他 gestureRecognizer 已经识别成功,此gestureRecognizer仍可识别
      - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
      {
          // This allows this gesture recognizer to persist, even if other gesture recognizers are recognized.
          return NO;
      }
    
      //若此 gestureRecognizer 成功,则按 css touch-action 的规则放行部分其他的 gestureRecognizers
      - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
      {
          ...
          //此处 _touchActionDelegate 判断 preventedGestureRecognizer 是不是 WebKit 挂载的相应的手势识别器
          auto mayPan = [_touchActionDelegate gestureRecognizerMayPanWebView:preventedGestureRecognizer];
          auto mayPinchToZoom = [_touchActionDelegate gestureRecognizerMayPinchToZoomWebView:preventedGestureRecognizer];
          auto mayDoubleTapToZoom = [_touchActionDelegate gestureRecognizerMayDoubleTapToZoomWebView:preventedGestureRecognizer];
    
          //不是webkit挂载的,则不禁止
          if (!mayPan && !mayPinchToZoom && !mayDoubleTapToZoom)
              return NO;
    
          //以下即是 css touch-action 的规则
          // Now that we've established that this gesture recognizer may yield an interaction that is preventable by the "touch-action"
          // CSS property we iterate over all active touches, check whether that touch matches the gesture recognizer, see if we have
          // any touch-action specified for it, and then check for each type of interaction whether the touch-action property has a
          // value that should prevent the interaction.
          auto* activeTouches = [_touchActionDelegate touchActionActiveTouches];
          for (NSNumber *touchIdentifier in activeTouches) {
              auto iterator = _touchActionsByTouchIdentifier.find([touchIdentifier unsignedIntegerValue]);
              if (iterator != _touchActionsByTouchIdentifier.end() && [[activeTouches objectForKey:touchIdentifier].gestureRecognizers containsObject:preventedGestureRecognizer]) {
                  // 设置了 pan-x/pan-y/manipulation 时,pan 手势才能生效
                  // Panning is only allowed if "pan-x", "pan-y" or "manipulation" is specified. Additional work is needed to respect individual values, but this takes
                  // care of the case where no panning is allowed.
                  if (mayPan && !iterator->value.containsAny({ WebCore::TouchAction::PanX, WebCore::TouchAction::PanY, WebCore::TouchAction::Manipulation }))
                      return YES;
                  // 设置了 pinch-zoom/manipulation 时,pinchToZoom 手势才能生效               
                  // Pinch-to-zoom is only allowed if "pinch-zoom" or "manipulation" is specified.
                  if (mayPinchToZoom && !iterator->value.containsAny({ WebCore::TouchAction::PinchZoom, WebCore::TouchAction::Manipulation }))
                      return YES;
                      
                  // 设置了 none 时,双击放大手势才能生效
                  // Double-tap-to-zoom is only disallowed if "none" is specified.
                  if (mayDoubleTapToZoom && iterator->value.contains(WebCore::TouchAction::None))
                      return YES;
              }
          }
    
          return NO;
      }
    

4. 多个 gestureRecognizer 之间的冲突解决

WKContentView 中挂载了很多不同的 gestureRecognizer[3],要解决这些 gestureRecognizer 之间的冲突,需要重载以下方法,并在其中实现冲突解决的业务逻辑,详见下述代码中的注释:

//WKContentView 

//工具方法
//static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UIGestureRecognizer *x, UIGestureRecognizer *y)
//{
//    return (a == x && b == y) || (b == x && a == y);
//}

//此方法指定哪些 gestureRecognizer 可以同时识别,而不是一个识别后就给别人发 touchesCancel
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
    ...
    
    // WKDeferringGestureRecognizer 和 toucheEventGestureRecognizer 是无冲突的
    for (WKDeferringGestureRecognizer *gesture in self._deferringGestureRecognizers) {
        //isSamePair用来判断四个入参中前两个与后两个是否是相同的二元组
        if (isSamePair(gestureRecognizer, otherGestureRecognizer, _touchEventGestureRecognizer.get(), gesture))
            return YES;
    }

    ...

    //WKDeferringGestureRecognizer 之间是无冲突的
    if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class] && [otherGestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class])
        return YES;

    //高亮手势和长按手势不冲突
    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _longPressGestureRecognizer.get()))
        return YES;

#if HAVE(UIKIT_WITH_MOUSE_SUPPORT)
    //多个鼠标手势之间不冲突
    if ([gestureRecognizer isKindOfClass:[WKMouseGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[WKMouseGestureRecognizer class]])
        return YES;
#endif

#if PLATFORM(MACCATALYST)
    //放大镜和用力长按文字手势不冲突
    if (isSamePair(gestureRecognizer, otherGestureRecognizer, [_textInteractionAssistant loupeGesture], [_textInteractionAssistant forcePressGesture]))
        return YES;

    //单击和放大镜手势不冲突
    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), [_textInteractionAssistant loupeGesture]))
        return YES;

    //查找与长按手势不冲突
    if (([gestureRecognizer isKindOfClass:[_UILookupGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) || ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] && [gestureRecognizer isKindOfClass:[_UILookupGestureRecognizer class]]))
        return YES;
#endif // PLATFORM(MACCATALYST)

    if (gestureRecognizer == _highlightLongPressGestureRecognizer.get() || otherGestureRecognizer == _highlightLongPressGestureRecognizer.get()) {
        auto forcePressGesture = [_textInteractionAssistant forcePressGesture];
        if (gestureRecognizer == forcePressGesture || otherGestureRecognizer == forcePressGesture)
            return YES;

        auto loupeGesture = [_textInteractionAssistant loupeGesture];
        //放大镜手势不冲突
        if (gestureRecognizer == loupeGesture || otherGestureRecognizer == loupeGesture)
            return YES;
            
        //1.5次点击手势不冲突
        if ([gestureRecognizer isKindOfClass:tapAndAHalfRecognizerClass()] || [otherGestureRecognizer isKindOfClass:tapAndAHalfRecognizerClass()])
            return YES;
    }

    //以下逻辑注释从略,有兴趣的读者可细读 WebKit 源码
    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), [_textInteractionAssistant singleTapGesture]))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), _nonBlockingDoubleTapGestureRecognizer.get()))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _nonBlockingDoubleTapGestureRecognizer.get()))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _previewSecondaryGestureRecognizer.get()))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _previewGestureRecognizer.get()))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _nonBlockingDoubleTapGestureRecognizer.get(), _doubleTapGestureRecognizerForDoubleClick.get()))
        return YES;

    if (isSamePair(gestureRecognizer, otherGestureRecognizer, _doubleTapGestureRecognizer.get(), _doubleTapGestureRecognizerForDoubleClick.get()))
        return YES;

#if ENABLE(IMAGE_EXTRACTION)
    if (gestureRecognizer == _imageExtractionGestureRecognizer || gestureRecognizer == _imageExtractionTimeoutGestureRecognizer)
        return YES;
#endif

    return NO;
}

//此方法用于指明各 gestureRecognizer 之间的优先级,只有otherGestureRecognizer识别失败之后,gestureRecognizer才能识别
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    //普通触摸事件优先级低于左、右滑导航(网页前进、后退)的手势
    if (gestureRecognizer == _touchEventGestureRecognizer && [_webView _isNavigationSwipeGestureRecognizer:otherGestureRecognizer])
        return YES;

    //对于 deferringGestureRecognizer 来说,如果它需要延迟 gestureRecognizer(事实上由 deferringGestureRecognizer的delegate来决定),则在此处指定其为高优先级
    if ([otherGestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class])
        return [(WKDeferringGestureRecognizer *)otherGestureRecognizer shouldDeferGestureRecognizer:gestureRecognizer];

    return NO;
}

//指定各 gestureRecognizer 之间的优先级,对于 deferringGestureRecognizer 来说,如果它需要延迟 otherGestureRecognizer,则这里指定其优先级
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class])
        return [(WKDeferringGestureRecognizer *)gestureRecognizer shouldDeferGestureRecognizer:otherGestureRecognizer];

    return NO;
}

5. 用 WKDeferringGestureRecognizer 来延迟其他 gestureRecognizer 的识别

延迟其他 gestureRecognizer 的识别主要有几下几种应用场景:

  • 同一个 view 上如果挂载多个 gestureRecognizer,而这些不同的 recognizer 所能识别的 touch 序列之间又包含共同前缀序列时,就需要对这些 recognizer 的成功识别做一个延迟,以保证所有的 recognizer 都有机会被识别。

  • 另一个场景是前端支持在 touchstart 等事件处理函数中禁用默认手势(event.preventDefault()),这就要求在处理 web 中的 touchstart 等事件时暂时延缓其他默认手势识别。

  • 而如果用户在 scrollView 滚动过程发起了触摸手势,则新手势不应该被延迟。

WKDeferringGestureRecognizer 实现延迟其他 gestureRecognizer 识别过程的主要机制为:

  • 在 WKDeferringGestureRecognizer 的 touchesBegan/touchesEnded 方法中,询问它的 delegate 是否要在此时 defer 此 event。如果不需要 defer,则直接将self.state = UIGestureRecognizerStateFailed,这时它对其他手势识别不造成影响;如果需要 defer,则不改变其手势识别状态,这样其他依赖它 fail 以后才能识别的 gestureRecognizer 的相关识别操作将被延迟。

  • touchesBegin 时判断是否 defer 的逻辑是看对应的 touch.view 是不是 scrollView,而且 scrollView 是否正在交互中(SPI: _isInterruptingDeceleration),如果是,则不 defer,否则 defer

  • touchesEnd 时则判断当前是否正在处理前端的 touchstart 事件(此事件可以阻止其他手势),如果是,则 defer

  • canBePreventedByGestureRecognizer 直接返回 NO,表示不会由于别的 gestureRecognizer 的识别成功而被强制 cancel 掉

//WKDeferringGestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    
    //若delegate认为需要defer,则直接return,不设置failed状态,这样后面依赖它 fail 才能进行的操作将被延迟。
    if ([_deferringGestureDelegate deferringGestureRecognizer:self shouldDeferGesturesAfterBeginningTouchesWithEvent:event])
        return;

    self.state = UIGestureRecognizerStateFailed;
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];

    if (self.state != UIGestureRecognizerStatePossible)
        return;

    if ([_deferringGestureDelegate deferringGestureRecognizer:self shouldDeferGesturesAfterEndingTouchesWithEvent:event])
        return;

    self.state = UIGestureRecognizerStateFailed;
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    self.state = UIGestureRecognizerStateFailed;
}

//不能被其他gestureRecognizer取消
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
    return NO;
}

  • 在 WKContentView 的手势冲突处理函数中,会调用以下方法来解决手势前缀序列问题

    //手势冲突相关方法调用链:
      -[WKContentView gestureRecognizer:shouldRequireFailureOfGestureRecognizer:] 调用:
          -[WKDeferringGestureRecognizer shouldDeferGestureRecognizer] 调用:
              -[WKContentView deferringGestureRecognizer:shouldDeferOtherGestureRecognizer:]
    
    
    //WKContentView
    
    //判断此 deferringGestureRecognizer 是否需要延迟 gestureRecognizer
    - (BOOL)deferringGestureRecognizer:(WKDeferringGestureRecognizer *)deferringGestureRecognizer shouldDeferOtherGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
      {
      #if ENABLE(IOS_TOUCH_EVENTS)
              //页面前进、后退手势不应该被延迟
          if ([_webView _isNavigationSwipeGestureRecognizer:gestureRecognizer])
              return NO;
    
              //判断手势对在的view是否挂载于webview视图树上
          auto webView = _webView.getAutoreleased();
          auto view = gestureRecognizer.view;
          BOOL gestureIsInstalledOnOrUnderWebView = NO;
          while (view) {
              if (view == webView) {
                  gestureIsInstalledOnOrUnderWebView = YES;
                  break;
              }
              view = view.superview;
          }
              //非 webview 视图树上的手势不应该被延迟
          if (!gestureIsInstalledOnOrUnderWebView)
              return NO;
    
              //其他 deferringGestureRecognizer 不应该被延迟
          if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class])
              return NO;
    
              //web touch 的手势不应该被延迟
          if (gestureRecognizer == _touchEventGestureRecognizer)
              return NO;
    
          auto mayDelayResetOfContainingSubgraph = [&](UIGestureRecognizer *gesture) -> BOOL {
      #if USE(UICONTEXTMENU) && HAVE(LINK_PREVIEW)
              if (gesture == [_contextMenuInteraction gestureRecognizerForFailureRelationships])
                  return YES;
      #endif
    
      #if ENABLE(DRAG_SUPPORT)
              if (gesture.delegate == [_dragInteraction _initiationDriver])
                  return YES;
      #endif
    
                  // 1.5次点击手势应该延迟
              if ([gesture isKindOfClass:tapAndAHalfRecognizerClass()])
                  return YES;
    
                  // 放大镜手势应该延迟
              if (gesture == [_textInteractionAssistant loupeGesture])
                  return YES;
    
                  // 单指多次点击手势应该被延迟
              if ([gesture isKindOfClass:UITapGestureRecognizer.class]) {
                  UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)gesture;
                  return tapGesture.numberOfTapsRequired > 1 && tapGesture.numberOfTouchesRequired < 2;
              }
    
              return NO;
          };
    
              //双击、单击手势应该延迟
          if (gestureRecognizer == _doubleTapGestureRecognizer || gestureRecognizer == _singleTapGestureRecognizer)
              return deferringGestureRecognizer == _deferringGestureRecognizerForSyntheticTapGestures;
              
          if (mayDelayResetOfContainingSubgraph(gestureRecognizer))
              return deferringGestureRecognizer == _deferringGestureRecognizerForDelayedResettableGestures;
    
          return deferringGestureRecognizer == _deferringGestureRecognizerForImmediatelyResettableGestures;
      #else
          UNUSED_PARAM(deferringGestureRecognizer);
          UNUSED_PARAM(gestureRecognizer);
          return NO;
      #endif
      }
    

参考资料