iOS触摸事件响应链剖析
最近优化项目,遇到一个需求。类似就是:两个View,viewA在viewB上面,我要透过viewA能对viewB进行点击滑动操作。
简单粗暴的方法
在viewA这个类中,重写下面的方法。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
}
这个方法的作用是用来判断,操作的触摸点是否在当前视图上。方法返回YES:触摸点在当前视图上,视图会响应事件。返回NO:触摸点不在当前视图上,视图不会响应事件。
还有,我们可以根据参数point来判断,进行点击区域的选择,可以实现,一个视图View,前半部分能响应点击,后半部分不响应点击。
所以viewA中重写这个方法让它返回NO,我们在点击到viewA上的时候,viewA不会响应,退而其次viewB会响应我们操作。这样就做到了穿透viewA对viewB进行操作。
为什么viewA不响应操作,viewB就行响应呢?这这涉及到响应链
响应者链剖析
首先说一下UIResponder,它响应用户的操作处理各种事件。UIView,UIViewController都是继承于它。而UIWindow,UILabel,UIImageView是继承于UIView。所以他们都能成为响应者,成为响应者链的一环。只有熟悉UIKit继承树,才能更好地理解响应者链。
UIKit继承树.jpg
当一个触摸事件产生后,系统会分为两步来处理:事件的传递+事件的响应
事件的传递
当触摸屏幕的时候,系统是这样传递这个触摸事件的:
- 加入到一个由UIApplication管理的事件队列中(队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列中)
- UIApplication会发送事件给应用程序的主窗口UIWindow。
- 主窗口UIWindow会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件(也就是把事件传递给那个最适合的UIView)
说一下这个方法:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
}
它的工作流程是:
- 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内
- 若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil
- 若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕。
- 若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束
- 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)
总结;每个view都有这个方法,用来处理用户的操作事件。它返回:self,代表这个view会接受用户的操作事件,返回:nil,则代表这个view不会接受用户的操作事件。
从以上还可以看出,视图在项目中的结构是以树状形式存在的。 视图树状图.png事件的响应
经过以上的事件的传递过程,事件已经传递给系统认为最适合的View了。接下来就是处理这个事件。
处理事件方法:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
}
但是最适合的,不一定就能一定能处理,如果,这个View不能处理这个事件则会将这个事件上抛,就是按照事件传递下来的路线上抛。
大致流程如下:
- 不能处理将事件传递给其上级视图(View的superView);
- 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller;
- 首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;
- 一直到 window,如果window还是不能处理此事件则继续交给application处理;
- 如果最后application还是不能处理此事件则将其丢弃;