在 iOS 开发中,当用户用手指点击了一下屏幕,会发生什么呢?系统是怎么判断用户点击的位置呢?咱们开发者又如何作出“没有bug”的交互呢?带着这些疑问,咱们一块儿谈谈事件的分发与响应。git
顾名思义,事件就是发生的一件事,对于APP来讲,就是发生的一个操做。具体的就是用户点击一下屏幕就会出现一个事件(体现为一个UIEvent
),即一个触摸事件。其实,对于 iOS 设备的用户来讲,他们操做设备的方式主要有四种方式:触摸屏幕、晃动设备、经过遥控设施控制设备、按压屏幕。 对应的事件类型UIEventType
有如下三种:github
咱们的主题是探索用户用手指点击屏幕会发生什么,因此咱们将注意力放在触摸事件
上。数组
上面咱们了解到,当咱们点击了屏幕,就会出现一个事件。既然事件出现了,那么就须要一个一个响应和处理这个事件的对象,那就是咱们的响应者对象。这些响应者对象都有一个共同的特征,就是他们都继承自UIResponder
。咱们熟知的响应者对象有UIApplication
、UIWindow
、 UIViewController
和全部继承自UIView
的 UIKit 类bash
UIResponderide
在触摸屏幕的事件中:测试
上面介绍了响应者对象,也知道了UIApplication
、UIWindow
、 UIViewController
、UIView
这些都是响应者。那么一个 APP 会存在不少响应者对象。由这一系列的响应者对象就构成了一个层次结构,那就是响应者链条。ui
从上图中能够看到,响应者链条有如下特色:spa
UIView
)构成的;UIViewController
)的,那么下一个响应者是该视图控制器,而后再将事件响应到它的父视图(Super View
)中;UIViewController
),那么下一个响应者就直接是它的父视图(Super View
);UIWindow
)UIApplication
),也是响应者链条的终点回到开篇的状况,当用户点击了一下屏幕。系统检测到用户的触摸事件,就会将其打包成一个事件(即UIEvent
对象),并将这个UIEvent
对象放入 Application 的事件队列中。这时系统只是知道有这么一个事件发生,虽然响应者链条中有不少有处理事件能力的响应者,可是它不知道谁才是响应这个事件的最佳人选。 所以,系统会从UIApplication
开始,顺着响应者链条向上寻找那个最佳的人选。这个寻找的过程就是事件的分发过程。code
UIApplication
将这个事件从事件队列中拿出来,从顶部开始询问谁才是最佳人选;UIWindow
会最早获取到事件,并开始使用hitTest:withEvent:
来判断下面他的子控件中谁才是最佳人选;UIView
继续询问他的子视图是否是最佳人选;UIView
不是被点击的的视图,orz,上一个UIView
就是最佳人选了。从用户视角来看,系统经过hitTest:withEvent:
方法,从视图的底部一直向表面寻找最佳人选。由于是一直查找,只有在全部的查找都完成了,判断出当前视图没有子视图或者他的子视图都不适合了,那么当前视图就是最佳人选了。(因此你只是点了一个你一眼就看中的视图,其实系统是从底部开始,一顿连续操做才找到你想要的东西[汗颜])cdn
上面的事件分发过程当中,大量使用了hitTest:withEvent:
这个方法,它的处理流程以下:
pointInside:withEvent:
方法,判断触摸点是否在当前视图内;
NO
,则hitTest:withEvent:
返回nil
;YES
,则向当前视图的全部子视图发送hitTest:withEvent:
消息,全部子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews
数组的末尾向前遍历。hitTest:withEvent:
方法返回此对象,处理结束;nil
,则hitTest:withEvent:
方法返回自身,即self
,处理结束。下面咱们用一个图解来理解一下这个hitTest:withEvent:
:
假如用户点击了View D
,结合上图详细介绍一下hitTest:withEvent:
过程: (hitTest:withEvent:
简称hitTest
,pointInside:withEvent:
简称pointInside
,View X
简称X
)
hitTest
;hitTest
。
hitTest:withEvent:
返回 nil;hitTest
时返回了 nil);hitTest
会将 D 返回,再往回回溯,就是 C 的hitTest
返回 D,A 的hitTest
返回 D。至此,本次点击事件的第一响应者就经过响应者链的事件分发逻辑成功找到了
除了使用pointInside:withEvent:
判断是不是响应者,还有下面三种状况会使hitTest:withEvent:
返回 nil:
hidden=YES
的视图;userInteractionEnabled=YES
的视图;alpha<0.01
的视图。所以hitTest:withEvent:
的实现多是:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
复制代码
前面说了一大堆事件的分发,其实就是为了找到响应事件的最佳人选,这个最佳人选就是在介绍响应者链条的时候,最底下的那个View。从这个 View 开始咱们沿着响应者链条的方向进行响应。
开篇咱们的说的是用户点击屏幕的场景,所以,响应者会按照当前UITouch
的所处阶段使用下面的方法进行响应:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
复制代码
[super touches...]
将这个触摸事件继续分发给父控件的对应方法处理。而后父控件还能够将该事件继续向上传递,直到传递给UIApplication对象。这一系列的响应者对象就构成了一个响应者链条。[super toucher...]
事件不会继续沿着响应者链条进行响应事件的分发和响应都是在响应者链条上进行的,只不过是二者传递的方向不一样。
UIViewController
,这里说明一下他的位置:
ViewController
的事至此,咱们已经大概了解了当用户用手指点击了一下屏幕,会发生什么。
经过对这些的了解,咱们能够经过使用下面两种方式来实现一些特殊需求:
- 重写 UIView 中的
hitTest:withEvent:
来影响事件分发- 重写 UIResponder 中的
touches
系列方法来影响事件响应
我在测试hitTest:withEvent:
的过程当中,经过运行时给每一个hitTest:withEvent:
都添加了打印方法,在点击绿色的B View的时候出现了下面的重复寻找的状况(不单只点击B View时候有出现)
这个现象我不太会解释...但愿有人能够解答一下。