iOS 响应者链

96
小乡123
2018.06.10 15:20* 字数 3150

为了方便理解,会分为三步去解说, 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属性处理,然后返回该视图即可.

开学了
Web note ad 1