WKWebView 中的手势识别

发布于 2023-10-03 14:50:08 字数 28124 浏览 39 评论 0

本文主要以 WebKit 源码中对 WKWebView 所支持的复杂手势处理逻辑为对象,研究学习 iOS 系统中手势处理的高级用法。

一、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
  }

参考资料

  • [1] iOS 中的事件响应:https://www.jianshu.com/p/c294d1bd963d
  • [2] CSS touch-action 文档:https://developer.mozilla.org/zh-CN/docs/Web/CSS/touch-action
  • [3] WKWebView 中各 gestureRecognizer 的具体作用:https://blog.csdn.net/hursing/article/details/8688869

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

悲念泪

暂无简介

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文