iOS触摸事件
事件的生命周期
1、系统响应阶段:触摸屏幕→IOKit(IOHIDEvent)→通过mach port(IPC进程间通信)转发给SpringBoard(IOHIDEvent)→SpringBoard通过当前桌面状态判断前台运行的app,并通过mach port转发给当前app
2、应用内部:当前app主线程RunLoop的Source1(监听mach port消息,接收SpringBoard事件)触发,并在Source0回调内部把IOHIDEvent封装为UIEvent,调用UIApplication的sendEvent把UIEvent传给UIWindow,开始通过hitTest:view寻找最佳响应者(UIResponder),找到最佳响应者后,事件开始在响应链中传递,最终被某个响应者/手势识别器/Target-Action模式捕捉并消耗掉,或没有找到任何响应对象后释放。
注:Mach Port进程端口,各进程利用它进行通信。
注:SpringBoard是一个系统进程,即桌面系统,统一管理和分发触摸事件。
传递链
事件的传递和分发,其实是寻找最佳响应者的过程(Hit-Testing 命中检测)。事件的传递起源于触摸状态的变化,一个点击会触发两次事件的传递(begin到end,touch的状态发生变化)
核心方法
1、- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {} // 视图是否能够响应事件
2、- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {} // 触摸点是否在hitTest方法返回的视图内
传递过程:UIApplication
→调用UIWindow的hitTest(多个window询问后显示的window(所以新添加的window记得makeKeyAndVisible))
→递归询问子视图是否能响应事件(实现:判断3种无法响应的情况→判断触摸点(return nil),从后往前遍历子视图(i = count - 1),convertPoint坐标转换到子视图上,并递归调用hitTest询问子视图中的子视图,如果有合适的则return,没有则return self)
→Window的hitTest方法返回最佳响应者,告知UIApplication。
注:无法响应的3种情况userInteractionEnabled = NO;hidden = YES;alpha < 0.01
事件拦截:定制视图,自定义事件流向。如遇到超出TabBar坐标范围的TabButton时,重写TabBar的pointInside方法,将坐标convert到Button上,再调pointInside判断是否在Button上即可实现越界点击。
响应链
事件首先传递给最佳响应者(hit-tested view)响应,并在响应链中的传递。
响应过程
UIApplication通过Event中的Touch对象找到触摸所属的Window,将事件通过sendEvent传递给Window,Window也通过sendEvent将事件优先传递给最佳响应者(直接传递,因为此时Window已找到最佳响应者并保存了起来),然后通过nextResponder方法获取下一个响应者,形成响应链。
注:UIView的nextResponder是VC(如果此View是根视图)或其父视图;
VC的nextResponder是Window(如果此VC是根视图),如果VC是被present出来的,则是它的presenting view controller;
UIWindow的nextResponder是UIApplication;
UIApplication的nextResponder是AppDelegate。
事件的响应
UIResponder对象通过4个touch方法响应触摸事件,但默认不做事情,只单纯的继续传递,重写方法可以截获事件,进行操作。(如创建自定义视图,重写touchMoved方法,通过touches和event属性实现简单的视图拖动)
注:在寻找最佳响应者时,所属的window和view会绑定到touch对象上,以供事件的传递过程中找到视图,给响应者发送事件。
事件的拦截
拦截和响应都是通过touchBegan方法控制的,默认实现为将事件沿着响应链继续向下传递。
1、不拦截,默认继续传递。
2、拦截,不再传递:重写touchesBegan处理事件,且不调用父类的方法。
3、拦截,继续传递:重写处理并调用父类的touchesBegan。
注:UIScrollViewDelayedTouchesBeganGestureRecognizer属性和手势延迟0.15秒的属性类似,所以,当视图中有手势和TableView共存时1、轻点手势只会执行手势而事件不会到达最佳响应者;2、短按会超过0.15到达最佳响应者但会因为手势拦截事件而被cancel;3、长按会因为手势识别失败,事件传递给最佳响应者执行cell的selected。
超级总结
1、UIWindow是分发事件和响应事件的重要角色,是事件的起点:寻找最佳响应者时从Window开始寻找子视图,找到后也是由Window通过sendEvent:将事件传递给最佳响应者(hit-tested view)。
2、响应链中,Control上没有Gesture优先响应Control,Responder优先级最低,甚至会被视图层级低于自己的Gesture打断并Cancel掉Touch状态。
UIEvent
事件本身,type标识事件类型(触摸,加速计等),allTouchs属性包含了如多个手指产生的所有触摸对象(UITouch)的集合。
UITouch
源起触摸,封装在UIEvent内部,在事件传递时用于判断hitTest-view和确定GestureRecognizers。1手指1触摸生成1个UITouch;N手指1触摸生成N个UITouch对象;N手指N触摸,通过触摸位置判断是更新上次的还是再生成一个UITouch。手指离开屏幕一段时间后,确定UITouch不再更新才会释放。
UIResponder
响应者对象,具备响应事件的能力,因为其提供了4个处理触摸事件的Touch方法:Began、Moved、Ended、Cancelled,在接收到事件时调用,可以做出响应。如UIView/UIViewController/UIApplication/AppDelegate。
UIControl
以Target-Action模式处理触摸事件,如UIButton、UISwitch。UIControl跟踪到触摸事件时会向Target发送事件以执行Action(只接收单点触控)。跟踪的4个方法:beginTrackingWithTouch:(UITouch *)touch、continueTracking、endTracking、cancelTracking。UIControl继承UIView,也具备普通UIResponder的身份,也有touch的4个方法,但默认实现与本类不同,如touchesBegan方法内部会调用beginTracking。
响应过程:先通过addTarget:action:forControlEvents:(UIControlEvents有许多关于交互事件的枚举)添加处理事件的target和action;当UIControl监听到事件时,sendAction把target、action和event都发给Application,再通过sendAction:from:to:forEvent向target发action(如果target为空,Application会在响应链中自己找能响应aciton的对象)
UIGestureRecognizer
同样具有响应事件的能力,分为离散型(tap、swipe)和持续型。4个处理事件的方法跟UIResponder一样但无关(在UIGestureRecognizerSubclass中声明)。
手势识别成功后的处理:事件响应在Source0回调的_UIApplicationHandleEventQueue()方法中,如果识别成了UIGesture手势时,首先调用Cancel打断当前的touch系列回调,然后将UIGestureRecognizer标记为待处理。当监听到BeforeWait事件时,回调函数_UIGestureRecognizerUpdateObserver()内部会获取所有标记待处理的手势,并执行手势回调。每当UIGestureRecognizer变化(创建销毁状态改变)时,RecognizerUpdate回调都会进行相应的处理。
离散型手势对响应链的影响
UIApplication在把事件传递给最佳响应者之前,会将事件先传递给相关的手势识别器开始识别,如果成功识别则会取消hit-tested view对事件的响应。但最佳响应者的touchBegan会先于手势的action执行,因为手势的识别成功(status = .recognized)的时机是稍晚的。但重写手势的touchBegan方法即可证明UIWindow先把事件传递给了手势。-[TapGestureRecognizer touchesBegan:withEvent:]-[View touchesBegan:withEvent:]-[TapGestureRecognizer touchesEnded:withEvent:]Gesture Taped Action-[View touchesCancelled:withEvent:]注:如何优先传递给手势识别器?在hit-test过程中收集了UIGestureRecognizer的数组,保存在了event绑定的touch上。
持续型手势对响应链的影响
一个滑动的交互:pan手势识别过程中,连续事件会先传递给最佳响应者持续调用touchesMoved;pan手势识别成功后执行action,并通知Application去cancel掉响应者对事件的响应,之后都由手势接收事件并响应。若没有识别成功,则会一直传递给最佳响应者。
UIGestureRecognizer的三个属性轻点、短按、长按
cancelsTouchesInView:决定手势识别成功后,是否cancel响应链中响应者的响应(拦截并阻断),默认是YES
delaysTouchesBegan:是否在手势识别期间,就阻断事件传递给最佳响应者,默认是NO
delaysTouchesEnded:手势识别失败且触摸结束时,是否延迟0.15秒通知最佳响应者调用touchesEnded以结束事件响应,默认是YES
相关问题
描述一下触摸事件的生命周期
分为系统响应阶段,和应用内部的传递、响应:IOKit到SpringBoard到APP(通过MachPort),应用内主线程RunLoop的Source回调中处理成UIEvent调UIApplication的sendEvent开始分发,通过hitTest找最佳响应者(传递链);响应者/手势识别器/Target-Action响应事件消耗或释放(响应链)。
系统是如何寻找最佳响应者的(传递链),如何在传递链中拦截事件
UIWindow中递归调用hitTest,判断三个交互条件+pointInside,满足则继续遍历子视图,否则返回自身。
重写pointInside可以扩大热区,拦截事件,自定义流向,最好不要调hitTest,因为实现不明,注意调super)
描述事件的响应过程(响应链),如何在响应链中拦截事件
事件由UIWindow先发送给最佳响应者,通过重写4个touch方法,截获处理后就终止(是否不再继续传递的标志是-是否调用super.touch方法),否则就nextResponder,期间手势识别器识别成功后会break掉touch的响应链。
UIGestureRecognizer对响应链的影响(离散型和持续型)
手势有识别过程,三个属性分别决定:识别成功后是否阻断响应链,是否开始识别时就阻断响应链,识别失败时候延迟通知最佳响应者调touchEnd。