本文系转载,原文地址为iOS触摸事件全家桶
现在,把胶卷回放到本章节开头的场景。给你一杯咖啡的时间看看能不能解释得通那几个现象了,不说了泡咖啡去了...
我肥来了!
先看现象二,短按 cell无法响应,日志如下:
-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]
这个日志和上面离散型手势Demo中打印的日志完全一致。短按后,BackView上的手势识别器先接收到事件,之后事件传递给hit-tested view,作为响应者链中一员的GLTableView的 touchesBegan:withEvent:
被调用;而后手势识别器成功识别了点击事件,action执行,同时通知Application取消响应链中的事件响应,GLTableView的 touchesCancelled:withEvent:
被调用。
因为事件被取消了,因此Cell无法响应点击。
再看现象三(长按Cell),长按cell能够响应,日志如下:
-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!
长按的过程中,一开始事件同样被传递给手势识别器和hit-tested view,作为响应链中一员的GLTableView的 touchesBegan:withEvent:
被调用;此后在长按的过程中,手势识别器一直在识别手势,直到一定时间后手势识别失败,才将事件的响应权完全交给响应链。当触摸结束的时候,GLTableView的 touchesEnded:withEvent:
被调用,同时Cell响应了点击。
OK,现在回到现象一(快速点击cell)。按照之前的分析,快速点击cell,讲道理不管是表现还是日志都应该和现象二一致才对。然而日志仅仅打印了手势识别器的action执行结果。分析一下原因:GLTableView的 touchesBegan
没有调用,说明事件没有传递给hit-tested view。那只有一种可能,就是事件被某个手势识别器拦截了。目前已知的手势识别器拦截事件的方法,就是设置 delaysTouchesBegan
为YES,在手势识别器未识别完成的情况下不会将事件传递给hit-tested view。然后事实上并没有进行这样的设置,那么问题可能出在别的手势识别器上。
Window的 sendEvent:
打个断点查看event上的touch对象维护的手势识别器数组:
捕获可疑对象:UIScrollViewDelayedTouchesBeganGestureRecognizer
,光看名字就觉得这货脱不了干系。从类名上猜测,这个手势识别器大概会延迟事件向响应链的传递。github上找到了该私有类的头文件:
@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
struct CGPoint {
float x;
float y;
} _startSceneReferenceLocation;
UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end
有一个_touchDelay变量,大概是用来控制延迟事件发送的。另外,方法列表里有个 sendTouchesShouldBeginForDelayedTouches:
方法,听名字似乎是在一段时间延迟后向响应链传递事件用的。为一探究竟,我创建了一个类hook了这个方法:
//TouchEventHook.m
+ (void)load{
Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
Method method = class_getClassMethod([self class], sel);
class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}
- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
[self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}
void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
Method newMethod = class_getInstanceMethod(aClass, newSEL);
method_exchangeImplementations(oldMethod, newMethod);
}
断点看一下点击cell后 hook_sendTouchesShouldBeginForDelayedTouches:
调用时的信息:
可以看到这个手势识别器的 _touchDelay 变量中,保存了一个计时器,以及一个长得很像延迟时间间隔的变量m_delay。现在,可以推测该手势识别器截断了事件并延迟0.15s才发送给hit-tested view。为验证猜测,我分别在Window的 sendEvent:
,hook_sendTouchesShouldBeginForDelayedTouches:
以及TableView的 touchesBegan:
中打印时间戳,若猜测成立,则应当前两者的调用时间相差0.15s左右,后两者的调用时间很接近。短按Cell后打印结果如下(不能快速点击,否则还没过延迟时间触摸就结束了,无法验证猜测):
-[GLWindow sendEvent:]调用时间戳 :
525252194779.07ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]调用时间戳 :
525252194930.91ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]调用时间戳 :
525252194931.24ms
-[GLTableView touchesBegan:withEvent:]调用时间戳 :
525252194931.76ms
因为有两个 UIScrollViewDelayedTouchesBeganGestureRecognizer
,所以 hook_sendTouchesShouldBeginForDelayedTouches
调了两次,两次的时间很接近。可以看到,结果完全符合猜测。
这样就都解释得通了。现象一由于点击后,UIScrollViewDelayedTouchesBeganGestureRecognizer
拦截了事件并延迟了0.15s发送。又因为点击时间比0.15s短,在发送事件前触摸就结束了,因此事件没有传递到hit-tested view,导致TableView的 touchBegin
没有调用。而现象二,由于短按的时间超过了0.15s,手势识别器拦截了事件并经过0.15s后,触摸还未结束,于是将事件传递给了hit-tested view,使得TableView接收到了事件。因此现象二的日志虽然和离散型手势Demo中的日志一致,但实际上前者的hit-tested view是在触摸后延迟了约0.15s左右才接收到触摸事件的。
至于现象四 ,你现在应该已经觉得理所当然了才对。
网友评论