iOS 响应者链
为了方便理解,会分为三步去解说, 1,点击事件找到对应的点击的视图的处理流程,2, 进行具体例子分析. 3, 常用的结论.
一. 点击事件处理流程
1. 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中
2. UIApplication会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)
3. 主窗口会首先调用pointInside:withEvent:, 该方法会根据触摸点来判断当前触摸是不是在当前视图内部, 并返回对应的标记:YES或NO
4. 主窗口会调用hitTest:withEvent:
4-1 如果第三步中的pointInside:withEvent:返回YES, 那么主窗口会它子视图的层级从上往下遍历, 然后子视图再调用自己的pointInside:withEvent:,
4-1-1 如果子视图pointInside:withEvent:返回NO, 那么就会在调用hitTest:withEvent:中返回nil, 那么该子视图的父视图会按着视图层级接着往下遍历他的子视图, 继续执行4-1
4-1-2 如果子视图pointInside:withEvent:返回YES,那么在调用hitTest:withEvent:中会返回当前的子视图, 则当前父视图认为已经找到了点击事件对应的视图, 就不再往下找了.
5. 找到点击的视图, 则开始判断该视图是否能响应点击事件
5-1 如果该视图处理了这个点击事件, 则执行该点击事件
5-2 如果该视图没有处理点击事件, 则响应者链上寻找该视图的下一个响应者, 判断下一个响应者能否处理该点击事件, 依次类推, 直到找到能处理该点击事件的响应者, 然后响应点击事件
5-3 如果该视图的响应者链上的所有对象都未处理该点击事件, 则丢弃该点击事件
二. 下面根据具体的例子分析:
下图中1, 2, 3 都是MainView的子视图, 4是1的子视图, 1,2,3的层级关系从上往下依次是3,2,1,视图3在MainView的最上层.
下面分为几种情况:
1. 点击MainView的空白区域
1-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
1-2 调用MainView最上层的子视图3, 这个视图3的pointInside返回NO,然后调用视图3的hitTest,并返回nil
1-3 调用MainView第二层子视图2, 视图2的pointInside返回NO, 然后调用视图2的hitTest并返回nil
1-4 调用MainView最下层子视图1, 视图1的pointInside返回NO, 然后调用视图1的hitTest并返回nil
1-5 MainView的所有子视图都寻找完成, 这时就调用到MainView的hitTest, 因为MainView所有的子视图hitTest都返回nil, 说明没点击到子视图, 这是MainView的hitTest会返回MainView实例对象, 这时,就找到了点击的视图MainView
1-6 MainView 处理点击事件, 如果不能处理, 则交由MainView的下一级响应者处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->NO ==>视图3 hitTest-->nil
==>视图2 pointInside-->NO ==>视图2 hitTest-->nil
==>视图1 pointInside-->NO ==>视图1 hitTest-->nil
==>MainView hitTest-->MainView
==>MainView click
2. 点击视图1中的橙色区域
2-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
2-2 调用MainView最上层的子视图3, 这个视图3的pointInside返回NO,然后调用视图3的hitTest,并返回nil
2-3 调用MainView第二层子视图2, 视图2的pointInside返回NO, 然后调用视图2的hitTest并返回nil
2-4 调用MainView最下层子视图1, 视图1的pointInside返回YES, 然后视图1会寻找自己的所有子视图,这时找到了视图4, 然后视图4调用自己的pointInside方法, 返回NO, 接着视图4调用自己的hitTest方法并返回nil, 视图1调用自己的hitTest并返回视图1的对象
2-5 MainView的所有子视图都寻找完成, 这时就调用到MainView的hitTest, 因为MainView所有的子视图1 hitTest都返回视图1对象, 说明没点击到子视图1, 这是MainView的hitTest会返回视图1的实例对象, 这时,就找到了点击的视图1
2-6 视图1处理点击事件, 如果不能处理, 则交由视图1的下一级响应者MainView处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->NO ==>视图3 hitTest-->nil
==>视图2 pointInside-->NO ==>视图2 hitTest-->nil
==>视图1 pointInside-->YES ==>视图4 pointInside-->NO ==>视图4 hitTest-->nil ==>视图1 hitTest-->视图1
==>MainView hitTest-->视图1
==>视图1 click
3. 点击视图2中的绿色区域
3-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
3-2 调用MainView最上层的子视图3, 这个视图3的pointInside返回NO,然后调用视图3的hitTest,并返回nil
3-3 调用MainView中间层的子视图2, 视图2的pointInside返回YES, 然后视图2会寻找自己的所有子视图,这时找不到其他子视图, 然后视图2调用自己的hitTest方法并返回视图2对象
3-4 MainView就不在接着遍历其他子视图, 调用自己的好hitTest,并返回视图2对象, 这时,就找到了点击的视图2
3-5 视图2处理点击事件, 如果不能处理, 则交由视图1的下一级响应者MainView处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->NO ==>视图3 hitTest-->nil
==>视图2 pointInside-->YES ==>视图2 hitTest-->视图2
==>MainView hitTest-->视图2
==>视图2 click
4. 点击视图3中的灰色区域
4-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
4-2 调用MainView最上层的子视图3, 视图3的pointInside返回YES, 然后视图3会寻找自己的所有子视图,这时找不到其他子视图, 然后视图3调用自己的hitTest方法并返回视图2对象
4-3 MainView就不在接着遍历其他子视图, 调用自己的好hitTest,并返回视图3对象, 这时,就找到了点击的视图3
4-4 视图3处理点击事件, 如果不能处理, 则交由视图3的下一级响应者视图1处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->YES ==>视图3 hitTest-->视图3
==>MainView hitTest-->视图3
==>视图3 click
5. 点击视图4和视图1的相交的蓝色区域
5-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
5-2 调用MainView最上层的子视图3, 这个视图3的pointInside返回NO,然后调用视图3的hitTest,并返回nil
5-3 调用MainView第二层子视图2, 视图2的pointInside返回NO, 然后调用视图2的hitTest并返回nil
5-4 调用MainView最下层子视图1, 视图1的pointInside返回YES, 然后视图1会寻找自己的所有子视图,这时找到了视图4, 然后视图4调用自己的pointInside方法, 返回YES, 接着视图4遍历自己的子视图,没有子视图,则调用自己的hitTest方法并返回视图4对象, 然后其父视图视图1调用自己的hitTest并返回视图4的对象
5-5 MainView的所有子视图都寻找完成, 这时就调用到MainView的hitTest, 因为MainView的子视图1 hitTest都返回视图4对象, 说明点击到子视图4, 这是MainView的hitTest会返回视图4的实例对象, 这时,就找到了点击的视图4
5-6 视图4处理点击事件, 如果不能处理, 则交由视图1的下一级响应者视图1处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->NO ==>视图3 hitTest-->nil
==>视图2 pointInside-->NO ==>视图2 hitTest-->nil
==>视图1 pointInside-->YES ==>视图4 pointInside-->YES ==>视图4 hitTest-->视图4 ==>视图1 hitTest-->视图4
==>MainView hitTest-->视图4
==>视图4 click
6. 点击视图4和视图1的非相交的蓝色区域
6-1 首先会会调用到MainView的pointInside方法, 并返回YES, 这时会寻找子视图,并调用其pointInside
6-2 调用MainView最上层的子视图3, 这个视图3的pointInside返回NO,然后调用视图3的hitTest,并返回nil
6-3 调用MainView第二层子视图2, 视图2的pointInside返回NO, 然后调用视图2的hitTest并返回nil
6-4 调用MainView最下层子视图1, 视图1的pointInside返回NO, 然后视图1不会寻找自己的子视图,视图1调用自己的hitTest方法并返回nil
6-5 MainView的所有子视图都寻找完成, 这时就调用到MainView的hitTest, 因为MainView所有的子视图hitTest都返回nil, 说明没点击到子视图, 这是MainView的hitTest会返回MainView实例对象, 这时,就找到了点击的视图MainView
6-6MainView 处理点击事件, 如果不能处理, 则交由MainView的下一级响应者处理, 依次类推, 如果一直到UIWindow都没有找到能响应该点击事件的响应者对象, 则丢弃该点击事件
对应的伪代码逻辑如下:
==>MainView pointInside-->YES
==>视图3 pointInside-->NO ==>视图3 hitTest-->nil
==>视图2 pointInside-->NO ==>视图2 hitTest-->nil
==>视图1 pointInside-->NO ==>视图1 hitTest-->nil
==>MainView hitTest-->MainView
==>MainView click
三. 一些结论
1. 如果一个视图的userInteractionEnabled被设置为NO, 或者alpha<0.01, 那么该视图的pointInside:withEvent:方法不会执行, 该视图hitTest:withEvent:方法会执行, 但是会返回nil, 即该视图的 [super hitTest:point withEvent:event]方法调用会返回nil.
2. 如果一个视图的backGroundColor被设置为clearColor, 那么该视图的触摸事件的处理不会受影响.
3. 增大一个视图的触摸面积,只需要在该视图的pointInside:withEvent:方法中根据point属性处理,然后返回YES即可. 不需要重写hitTest:withEvent:方法.
4. 如果视图超出它的父视图边界,还需要正常响应触摸事件, 需要重写hitTest:withEvent:方法, 在该方法中根据point属性处理,然后返回该视图即可.