WKWebView 中的手势识别
本文主要以 WebKit 源码中对 WKWebView 所支持的复杂手势处理逻辑为对象,研究学习 iOS 系统中手势处理的高级用法。
一、iOS 系统中手势处理的背景知识
在 iOS 系统中,屏幕点击事件从开始到结束主要经历以下步骤[1]:
- 用户点击屏幕,生成一个硬件触摸事件
- 操作系统将硬件触摸事件包装成 IOHIDEvent 后发送给 Springboard,Springboard 再将 IOHIDEvent 发给当前打开的 App 进程
- App 进程收到事件后,将主线程 Runloop 唤醒(Source1),Source1 回调中触发 Source0 回调,将 IOHIDEvent 包装成 UIEvent 对象,发送给顶层的 UIWindow(-[UIApplication sendEvent:])
- UIWindow 对 UIEvent 中的每个 UITouch 实例调用 hitTest 方法寻找其 hitTestView,并在 hitTest 递归调用过程中,记录最终的 hitTestView 及其各父视图上挂载的 gestureRecognizer(记录为 gestureRecognizers 属性)
- 同级的多个 view 之间 hitTest 调用顺序为逆序(后 addSubView 的先调用 hitTest)
- 默认根据 pointInside 方法的返回值来决定是否递归查找其子 view
- 将每个 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论