本文主要参考了 VV木公子(简书作者)的 史上最详细的iOS之事件的传递和响应机制
我按照自己的理解做了排版和一些表述的修改。
在开发过程中我们经常会遇到一些事件响应优先级的问题,通过搜索知道了hitTest,再根据hitTest去搜索一些类似问题,问题最终是解决了,但是我们得知道为什么是这么解决的。以下内容就是详细说明iOS的事件传递和响应机制。
(一)iOS中的事件
iOS的事件分为3大类型:
- 触摸事件
- 加速计事件
- 远程控制事件
本篇只讨论触摸事件
1.1 响应者对象(UIResponder)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
- UIApplication
- UIViewController
- UIView
UIViewController可以通过在.m中覆写相关方法来处理事件,例如touchesBegan、touchesMoved等;UIView则必须通过自定义子类来处理。
(二)iOS中事件的产生和传递
2.1 事件的产生
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。为什么是队列而不是栈?因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow)。
- 主窗口(keyWindow)会在视图层次结构中找到一个最合适的视图来处理触摸事件,寻找最合适的视图的关键就是hitTest:withEvent:方法。
2.2 事件的传递
事件的传递是自上到下的顺序,即UIApplication->window->处理事件最合适的view。
- 首先判断主窗口(keyWindow)自己是否能接受触摸事件
- 判断触摸点是否在自己身上
- 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
- 通过前3步寻找到了fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
- 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
上述流程的实现代码如下所示:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
知道了hitTest的作用,我们可以通过覆写它来达到干涉事件的传递。例如whiteView上有两个view,一个redView,一个greenView,redView先添加到父视图上,系统默认将事件传递给后添加的view也就是greenView上,如果我们想让redView响应事件可通过覆写whiteView的hitTest方法指定返回redView,再在redView的touch处理方法中执行就达到了事件拦截的目的。
UIView不能接收触摸事件的三种情况:
不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
-
透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES。
(三)iOS中事件的响应
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示:
3.1 响应者链的事件传递过程:
- 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图。
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。
- 如果window对象也不处理,则其将事件或消息传递给UIApplication对象。
- 如果UIApplication也不能处理该事件或消息,则将其丢弃。
(四)iOS事件传递和响应总结:
- 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> fit view,以上就是事件的传递,也就是寻找最合适的view的过程。
- 接下来是事件的响应。首先看fit view能否处理这个事件,如果不能则会将事件传递给其上级视图(fit view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。
- 在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法。
如果您觉得本文对您有所帮助,请点击「喜欢」来支持我,欢迎关注个人公众号「IT不难」,我将保持同步更新。
有任何疑问都可联系我,欢迎探讨。
微信号:xieguobihaha