UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。
_**UIResponder**不只用来接收事件,还可以处理和传递对应的事件,_如果当前响应者不能处理,则转发给其他合适的响应者处理。
应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。
查找第一响应者
基础API
查找第一响应者时,有两个非常关键的API,查找第一响应者就是通过不断调用子视图的这两个API完成的。
调用方法,获取到被点击的视图,也就是第一响应者。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:方法内部会通过调用这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
查找第一响应者
应用程序接收到事件后,将事件交给keyWindow并转发给根视图,根视图按照视图层级逐级遍历子视图,并且遍历的过程中不断判断视图范围,并最终找到第一响应者。
从keyWindow开始,向前逐级遍历子视图,不断调用UIView的hitTest:withEvent:方法,通过该方法查找在点击区域中的视图后,并继续调用返回视图的子视图的hitTest:withEvent:方法,以此类推。如果子视图不在点击区域或没有子视图,则当前视图就是第一响应者。
在hitTest:withEvent:方法中,会从上到下遍历子视图,并调用subViews的pointInside:withEvent:方法,来找到点击区域内且最上面的子视图。如果找到子视图则调用其hitTest:withEvent:方法,并继续执行这个流程,以此类推。如果子视图不在点击区域内,则忽略这个视图及其子视图,继续遍历其他视图。
可以通过重写对应的方法,控制这个遍历过程。通过重写pointInside:withEvent:方法,来做自己的判断并返回YES或NO,返回点击区域是否在视图上。通过重写hitTest:withEvent:方法,返回被点击的视图。
此方法在遍历视图时,忽略以下三种情况的视图,如果视图具有以下特征则忽略。但是视图的背景颜色是clearColor,并不在忽略范围内。
- 视图的hidden等于YES。
- 视图的alpha小于等于0.01。
- 视图的userInteractionEnabled为NO。
如果点击事件是发生在视图外,但在其子视图内部,子视图也不能接收事件并成为第一响应者。这是因为在其父视图进行hitTest:withEvent:的过程中,就会将其忽略掉。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:642363427不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
事件传递
传递过程
- UIApplication接收到事件,将事件传递给keyWindow。
- keyWindow遍历subViews的hitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。
- UIView的子视图也会遍历其subViews的hitTest:withEvent:方法,以此类推。
- 直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication。
- 在查找第一响应者的过程中,已经形成了一个响应者链。
- 应用程序会先调用第一响应者处理事件。
- 如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。
- 最后到UIApplication后仍然没有能处理该事件的对象,则该事件被废弃。