本文重点介绍了 WebKit 源码中 wkwebview 支持的复杂手势处理逻辑,并研究了手势处理在 iOS 系统中的高级用法。
在 iOS 中,屏幕点击事件从头到尾会经历以下步骤[1]
用户点击屏幕生成一个硬件触摸事件,操作系统将硬件触摸事件包装成 iohidevent 并发送到 Springboard,Springboard 再将 iohidevent 发送给当前打开的 app 进程,app 进程接收到该事件后,唤醒主线程 runloop(source1),并在 source1 中触发 source0 将 iohidevent 包装在 uiEvent 对象中, 发送到顶级 UIAwindow(-[UIInit sendevent:]) UIAwindow 调用 HitTest 方法查找 UITavice 中每个 UITuTouch 实例的 HitTestView,并记录最终的 HitTestView 和挂载在 HitTest 递归调用进程上的每个父视图 同一级别的多个视图之间的 GestureRecognizer(记录为 GestureRecognizers 属性)以相反的顺序调用(在 AddSubview 之后, 首先调用 HitTest),默认情况下,根据 PointInside 方法的返回值,它决定是否以递归方式查找其子视图,实现中的注意事项:
手动调用 hittest:withevent: 方法时,需要递归地对子视图进行 hittest 测试,方法是将坐标点转向目标视图的坐标系并调用 [super hittest:withevent:](默认逻辑)。
wkcontentView- (UIVet *)hitTest:(CGPorent)Point WithEvent:(UITature *)Event } 默认 HitTest 逻辑,递归遍历子视图 UIVerview* HitView = [Super HitTest:Point withEvent:Event:Event]; return hitview;}
通过重载 hittest:withevent: 方法,并在其中定义要直接测试或返回的特定视图。
需要注意的是,以下例程中没有调用 [super hittest:withevent:],即不遍历子视图做 hittest 的默认逻辑在 ** 中的注释中有详细说明
wkCompositin**iew- (UIVet *)HitTest:(CGPorent)Point WithEvent:(UITert) Event:(UITert) 具体实现为: uiView(wkhitTesting)- UIView *)Web FindDescendantViewAtPoint:(CGPorent)Point WithEvent:(UITat) Event 直接返回到这样的视图,而不是递归地查找子视图 if ([view iskindofclass:[wkchildscrollview class]]) ditto if ([view iskindofclass:webkit::scrollviewscrollindicatorclass()]view.superview iskindofclass:wkchildscrollview.class]) //ignoring other views } return nil;}
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 不允许使用触摸手势轻扫该元素。
实现方案(详见**片段和注释):
为了支持触摸操作,在 webkit 中定义了一个特殊的 wktouchactiongesturerecognizer,并将其作为最后一个手势识别器添加到 wkcontentview(请参阅第一部分中提到的事件调度优先级,最后一个手势识别器)。在 wkTouchActionGestureRecognizer 的 TouchesBegin、Touchesmoved 和 TouchesEnded 方法的实现中,直接调用 updateState 将手势识别状态设置为识别成功。这样,按照第一部分提到的处理手势冲突的逻辑,其他手势识别器和 hittestviews 会接收到触摸取消**,以达到阻止其他手势响应的效果注意:不是所有的手势都被阻止,哪些手势不能被阻止,还依赖于以下解决手势冲突的方法来实现 wkTouchActionGestureRecognizer 的 CanBePreventedByGestureRecognizer: 该方法返回 no,这意味着即使另一个手势识别器已被识别为成功,它仍然可以识别成功并继续接收触摸 消息 WKTicACTIONTegACTecker 的 CanPreventGestureCognizer: 方法允许或禁用 WKWebView 中的多个预定义手势,具体取决于触摸操作设置
wkTouchActionGestureRecognizer(作为添加到 wkContentView 的最后一个 GestureRecognizer) TouchBegin 设置 Recognized 状态,这可能会使其他 GestureRecognizer 无法识别 (Cancelled)- VoidTouchesBegan:(nsset * )touches withevent:(uievent *)event- (void)touchesmoved:(nsset *)touches withevent:(uievent *)event- (void)touchesended:(nsset *)touches withevent:(uievent *)event- (void) TouchesCancelled:(nsset *)Touches withEvent:(uievent *)Event 此方法将GestureRecognizer 设置为识别成功状态,允许其他 GestureRecognizers 或 HitTestView 停止接收事件(使用其他手势冲突解决方法) - void) UpdateState 即使已成功识别其他手势识别器,仍可识别此手势识别器- (bool)canbepreventedbygesturerecognizer:(uigesturerecognizer *)preventinggesturerecognizer 如果此手势识别器成功,请按 css touch-action 仅当设置了捏合缩放操作时,捏合缩放手势才生效,只有在以下情况下才允许捏合缩放"pinch-zoom" or "manipulation" is specified. if (maypinchtozoom &&iterator->value.containsany())return yes;如果未设置任何设置,则双击缩放手势以生效 只有在以下情况下才允许双击缩放"none" is specified. if (maydoubletaptozoom &&iterator->value.contains(webcore::touchaction::none)) return yes; }return no;}
wkContentView 中挂载了许多不同的 GestureRecognizers[3],要解决这些 GestureRecognizer 之间的冲突,您需要重载以下方法并在其中实现用于解决冲突的业务逻辑,详见下面的注释
wkContentView 工具方法 static inline bool issamepair(uigesturerecognizer *a, uigesturerecognizer *b, uigesturerecognizer *x, uigesturerecognizer *y) 此方法指定可以同时识别哪些手势识别器,而不是发送某人touchescancel- (bool)gesturerecognizer:(uigesturerecognizer *)gesturerecognizer shouldrecognizeSimultaneouslywithGesturerecognizer:(uigesturerecognizer*) othergesturerecognizer ..WkDeferringGestureRecognizer 之间没有冲突 if ([GestureRecognizer iskindofclass:wkdeferringGestureRecognizer.class] &othergesturerecognizer iskindofclass:wkdeferringgesturerecognizer.class]) return yes;如果 (issamepair(gesturerecognizer, othergesturerecognizer, highlightlongpressgesturerecognizer.),则突出显示手势和长按手势不冲突。get(),longpressgesturerecognizer.get())return yes;if h**e(uikit with mouse support) if ([GestureRecognizer IskindOfClass:[WkMouseGesturerecognizer class]] otherGesturerecognizer iskindofclass:[ wkmouseGesturerecognizer class]])返回 yes;Endif If Platform(MacCatalyst) 放大镜与硬按文本手势不冲突 if (issamepair(gesturerecognizer, othergesturerecognizer, [textinteractionassistant loupegesture], textinteractionassistant forcepressgesture]))return yes;单击和放大镜手势不冲突,如果 (issamepair(gesturerecognizer, othergesturerecognizer, singletapgesturerecognizer..)get(),textinteractionassistant loupegesture]))return yes;查找这是否与长按手势不冲突 (((GestureRecognizer isKindofClass:[ UiLookUpGestureRecognizer Class]] OtherGestureRecognizer isKindofClass:[UiLongpressGestureRecognizer Class]]) otherGestureRecognizer isKindofClass:[UiLongPressGestureRecognizer Class]] GestureRecognizer isKindofClass:[ UiLookUpGestureRecognizer Class]])返回 yes;#endif // platform(maccatalyst) if (gesturerecognizer == _highlightlongpressgesturerecognizer.get() othergesturerecognizer == _highlightlongpressgesturerecognizer.get())以下逻辑注释省略,感兴趣的读者可以阅读 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;} 此方法用于指示每个手势识别器之间的优先级,手势识别器只有在othergesturerecognizer无法识别后才会识别- (bool)gesturerecognizer:(uigesturerecognizer *)gesturerecognizer shouldrequirefailureofgesturerecognizer: (uigesturerecognizer *)othergesturerecognizer 指定每个手势识别器之间的优先级,对于 deferringgesturerecognizer,如果需要延迟 otherGestureCognizer,这里指定它的优先级 - (布尔值)GestureCoreCerver:(uiGestureCognDer*)GestureCognizer ShouldBeRequiredToFailbyGestureCognizer:(uiGestureCognizer *) otherGesturerecognizer
延迟识别其他手势识别器有几个主要用例:
如果多个手势识别器挂载在同一视图上,并且这些不同识别器可以识别的触摸序列包含共同的前缀序列,则需要延迟这些识别器的成功识别,以确保所有识别器都有机会被识别。 另一种方案是前端支持在事件处理程序(如 touchstart)中禁用默认手势 (event.)preventdefault())这要求在处理 Web 中的 touchstart 等事件时暂时推迟其他默认手势识别。如果用户在滚动视图期间启动触摸手势,则不应延迟新手势。 wkdeferringGestureRecognizer 实现延迟识别其他 GestureRecognizer 过程的主要机制是:
在 wkdeferringGestureRecognizer 的 TouchesStarted Touchesended 方法中,询问其委托是否要此时推迟此事件。 如果您不需要延迟,只需设置 self 即可State = UIIdementRecognizerStateFailed,在这种情况下,它对其他手势识别没有影响; 如果需要延迟,则其手势识别状态不会更改,因此在失败后依赖手势识别器进行识别的其他识别操作将被延迟。 touchesbegin 判断延迟是否基于对应的触摸的逻辑view 不是 scrollview,如果 scrollView 正在交互(spi:isInterruptingDeceleration),如果是,则不 defer,否则 defertouchesend,判断前端当前是否正在处理 touchStart 事件(此事件可以阻止其他手势),如果是,则 DeferCanBePreventedByGestureRecognizer 直接返回 no,表示不会因为成功识别其他 GestureRecognizer 而强制取消
wkdeferringGesturerecognizer- (void)TouchesBegin:(NSSET *)Touches withEvent:(UIevent *)Event- (void)TouchesEnded:(NSSET *)Touches withEvent:(UITature *)Event- (void) TouchesCancelled:(nsset *)Touches withevent:(uievent *)Event 不能被其他 GestureRecognizer 取消 - (bool)CanBePreventedByGestureRecognizer:(uiGestureRecognizer *) preventinggesturerecognizer
在 wkcontentview 的手势冲突处理程序中,调用以下方法来解决手势前缀序列问题。
手势冲突相关方法调用链:-[wkContentView GestureRecognizer:shouldrequirefailureofgesturerecognizer:] 调用:-[wkdeferringGestureRecognizer shoulddeferGesturerecognizer] 调用:-[ wkContentView DeferringGestureRecognizer:ShouldDeferOtherGestureRecognizer:] wkContentView 确定是否需要延迟此 DeferringGestureRecognizer GestureRecognizer- (bool) deferringGesturerecognizer:(wkdeferringgesturerecognizer *)deferringgesturerecognizerShouldDeferOtherGestureRecognizer:(UIEcementRecognizer *)GestureRecognizer 视图 = 视图。superview;如果 (!.)gestureisinstalledonorunderwebview) return no;如果出现以下情况,则不应延迟其他 DeferringGestureRecognizers。class]) return no;如果 (gesturerecognizer == toucheventgesturerecognizer) return no,则不应延迟 Web Touch 手势; auto maydelayresetofcontainingsubgraph = [&uigesturerecognizer *gesture) -bool 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 中的事件响应:CSS 触摸操作文档:每个 GestureRecognizer 在 WKWebView 中的作用: