本文系转载,原文地址为iOS触摸事件全家桶
经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:
- 将事件传递给最佳响应者响应
- 事件沿着响应链传递
事件响应的前奏
因为最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。首先,UIApplication将事件通过 sendEvent:
传递给事件所属的window,window同样通过 sendEvent:
再将事件传递给hit-tested view
,即最佳响应者。过程如下:
UIApplication ——> UIWindow ——> hit-tested view
以寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan:withEvent:
上断点查看调用栈就能看清这一过程:
那么问题又来了。这个过程中,假如应用中存在多个window对象,UIApplication是怎么知道要把事件传给哪个window的?window又是怎么知道哪个视图才是最佳响应者的呢?
其实简单思考一下,这两个过程都是传递事件的过程,涉及的方法都是 sendEvent:
,而该方法的参数(UIEvent对象)是唯一贯穿整个经过的线索,那么就可以大胆猜测必然是该触摸事件对象上绑定了这些信息。事实上之前在介绍UITouch的时候就说过touch对象保存了触摸所属的window及view,而event对象又绑定了touch对象,如此一来,是不是就说得通了。要是不信的话,那就自定义一个Window类,重写 sendEvent:
方法,捕捉该方法调用时参数event的状态,答案就显而易见了。
至于这两个属性是什么时候绑定到touch对象上的,必然是在hit-testing的过程中呗,仔细想想hit-testing干的不就是这个事儿吗~
事件的响应
前面介绍UIResponder
的时候说过,每个响应者必定都是UIResponder
对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder
对象默认都已经实现了这4个方法,但是默认不对事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。例如,通过重写 touchesMoved: withEvent:
方法实现简单的视图拖动。
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
每个响应触摸事件的方法都会接收两个参数,分别对应触摸对象集合
和事件对象
。通过监听触摸对象中保存的触摸点位置的变动,可以时时修改视图的位置。视图(UIView)作为响应者对象,本身已经实现了 touchesMoved: withEvent:
方法,因此要创建一个自定义视图(继承自UIView),重写该方法。
//MovedView
//重写touchesMoved方法(触摸滑动过程中持续调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//获取触摸对象
UITouch *touch = [touches anyObject];
//获取前一个触摸点位置
CGPoint prePoint = [touch previousLocationInView:self];
//获取当前触摸点位置
CGPoint curPoint = [touch locationInView:self];
//计算偏移量
CGFloat offsetX = curPoint.x - prePoint.x;
CGFloat offsetY = curPoint.y - prePoint.y;
//相对之前的位置偏移视图
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
每个响应者都有权决定是否执行对事件的响应,只要重写相关的触摸事件方法即可。
事件的传递(响应链)
前面一直在提最佳响应者,之所以称之为“最佳”,是因为其具备响应事件的最高优先权(响应链顶端的男人)。最佳响应者首先接收到事件,然后便拥有了对事件的绝对控制权:即它可以选择独吞这个事件,也可以将这个事件往下传递给其他响应者,这个由响应者构成的视图链就称之为响应链。
需要注意的是,上一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上一节所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。
响应者对于事件的操作方式:
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
- 不拦截,默认操作
事件会自动沿着默认的响应链往下传递 - 拦截,不再往下分发事件
重写touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
- 拦截,继续往下分发事件
重写touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
响应链中的事件传递规则:
每一个响应者对象(UIResponder对象)都有一个 nextResponder
方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。
对于响应者对象,默认的 nextResponder
实现如下:
- UIView
若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。 - UIViewController
若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。 - UIWindow
nextResponder为UIApplication对象。 - UIApplication
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
上图是官网对于响应链的示例展示,若触摸发生在UITextField上,则事件的传递顺序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder为UIWindow对象。
可以用以下方式打印一个响应链中的每一个响应对象,在最佳响应者的 touchBegin:withEvent:
方法中调用即可(别忘了调用父类的方法)
- (void)printResponderChain
{
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
以上一节原型按钮的案例为例,重写CircleButton的 touchBegin:withEvent:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
点击原型按钮的任意区域,打印出的完整响应链如下:
CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate
另外如果有需要,完全可以重写响应者的 nextResponder
方法来自定义响应链。