精简地说:iOS事件分为传递和响应两个部分。
事件传递(建立传递链):
iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
hittest的目的就是找到最终的传递链。
hitTest:withEvent:流程如下:
- 先判断当前视图hidden=YES,userInteractionEnabled=NO,alpha<0.01等属性,如果满足其中之一,返回nil。
- 再看当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
- 若返回NO,则hitTest:withEvent:返回nil;
- 若返回YES,则向当前视图的【一级子视图(subviews)递归发送】hitTest:withEvent:消息,所有子视图的遍历顺序是【从subviews数组的末尾向前遍历】,直到有子视图返回非空对象或者全部子视图遍历完毕;
- 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回自身。
提醒:
hittest返回nil表示该条传递链已经终止,不是正确的传递链。
hittest返回非nil对象表示已经找到传递链的叶子节点,即找到正确的传递链。
hitTest:withEvent:遇到以下会返回nil。
1.hidden=YES的视图。
2.userInteractionEnabled=NO的视图(注意userInteractionEnabled是影响子视图事件传递,但不影响兄弟视图)
3.alpha<0.01的视图。
4.显示区域超过父视图bounds区域的视图。那么超出区域不能识别。当然,可以重写pointInside:withEvent:方法来识别。
hitTest:底层实现
- (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]) return nil;
// 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
int count = self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
// 没有找到比自己更合适的view
return self;
}
结合例子分析:
举例分析:用户点击了View D,下面结合上图介绍hit-test view的流程:
1、A是UIWindow的根视图,因此,UIWindwo对象会首先对A进行hit-test;
2、显然用户点击的范围是在A的范围内,因此,pointInside:withEvent:返回了YES,这时会继续检查A的子视图;
3、这时候会有两个分支,B和C:
C在subviews的末尾,先递归遍历C。点击的范围在C内,即C的pointInside:withEvent:返回YES;
4、这时候有D和E两个分支:
点击的范围不再E内,因此E的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;
点击的范围在D内,即D的pointInside:withEvent:返回YES,由于D没有子视图,因此,D的hitTest:withEvent:会将D返回,再往回回溯,就是C的hitTest:withEvent:返回D--->>A的hitTest:withEvent:返回D。
即A->C->E(返回nil)->D(返回D)->C(返回D)->A(返回D)
至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。
不难看出,这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。
另外hittest可能会被调用2-3次,这里请不要再hittest作业务相关操作,否则会导致执行多次。
事件响应(回溯响应链):
经过hittest后,我们已经找到了链尾【第一响应者】,这时候开始回溯响应操作。响应链的关系如图:
请注意:响应链在传递链的基础上增加了UIViewController。
NextResponder:
1.当一个view被添加到superView上的时候,它的nextResponder就会被指向它的superView;
2.当vc被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller;
(概括前两者就是:如果当前这个view是控制器的self.view,那么控制器就是上一个响应者 如果当前这个view不是控制器的view,那么父控件就是上一个响应者)
3.vc的nextResponder会被指向self.view的superView。
4.最顶级的vc的nextResponder指向UIWindow。
5.UIWindow的nextResponder指向UIApplication
在事件响应对象UIResponder(UIView和UIViewController都是继承UIResponder)中有对应的方法来分别处理这几个阶段的事件:
touchesBegan:NSArray<UITouch *>>withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
Touch默认流程(以单击为例)是:从响应链叶子节点开始回溯,先是TouchBegan回溯,然后是TouchEnded回溯。
举例:对于触摸事件来说,UIApplication会首先把事件交给keyWindow,Window会将事件交给UIGestureRecognizer
处理,如果UIGestureRecognizer
识别了传递过来的事件,则交给相对应的target去处理,回溯终止,事件不会再传递!当然这里也可以重写touchBegan等方法手动让手势继续往superview传从而实现多级响应。
如果UIGestureRecognizer
并没有识别传递过来的事件(可能是没有视图添加手势,也可能手势识别不成功),事件会传递到视图树形结构
touches方法实际上什么事都没做,UIView继承了它进行重写,就是把事件传递给nextResponder,相当于[self.nextResponder touchesBegan:touches withEvent:event]。所以当一个view没有重写touch事件,那么这个事件就会一直传递下去,直到UIApplication。如果重写了touch方法,这个view响应了事件之后,事件就被拦截了,它的nextResponder不会收到这个事件。这个时候如果想事件继续传递下去,可以调用[self.nextResponder touchesBegan:touches withEvent:event]
手势:
通过touches方法监听view触摸事件,有很明显的几个缺点:必须得自定义view、由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件、不容易区分用户的具体手势行为。
所以iOS把触摸事件做了封装, 对常用的手势进行了处理, 封装了6种常见的手势
UITapGestureRecognizer(敲击)
UILongPressGestureRecognizer(长按)
UISwipeGestureRecognizer(轻扫)
UIRotationGestureRecognizer(旋转)
UIPinchGestureRecognizer(捏合,用于缩放)
UIPanGestureRecognizer(拖拽)
UIControlEvent:
其实要了解UIControlEvent,必须简单说一下UITouch和UIEvent事件,都是和触摸相关,UIEvent是一系列UITouch的集合,在IOS中负责响应触摸事件。
UIControl是UIView的子类,当然也是UIResponder的子类。UIControl是诸如UIButton、UISwitch、UITextField等控件的父类,它本身也包含了一些属性和方法。
UIControl对象采用了一种新的事件处理机制,将触摸事件转换成简单操作,其实就是重写了UIResponder的方法中(如touchBegan:withEvent)中,即事件不再往上回溯响应。这样方便了事件处理,而不用每次都重写TouchBegan方法。
UIResponder可以参考这篇文章UIKit: UIResponder。
比如我点击一个UIButton,即使你未添加UIControlEventTouchUpInside,它的父类touchBegan也不会被调用。因为UIControl重写的方法touchBegan:withEvent并未调用[super touchBegan:withEvent]
UITapGestureRecognzier:
UITapGestureRecognzier其实就是对各类复杂触摸操作响应过程的一个封装。
在六种手势识别中,只有一种手势是离散手势,它就是UITapGestureRecognzier。离散手势的特点就是一旦识别就无法取消,而且只会调用一次手势操作事件(初始化手势时指定的触发方法)。换句话说其他五种手势是连续手势,连续手势的特点就是会多次调用手势操作事件,而且在连续手势识别后可以取消手势。【下图是手势状态图】
分别以UITap和UIPan两种手势说明流程:
UITap:TouchBegan回溯->Tap->TouchCancelled回溯。
UIPan:TouchBegan回溯->TouchMoved多次回溯->UIPanBegan-TouchCancelled回溯->UIPanChanged多次->UIPanEnded。
事件总结:
1.父视图不能接收事件,则子视图无法接受事件
2.子视图超出父视图的部分,不能接收事件
3.同一个父视图下,最上面的视图,首先遭遇事件,如果能够响应,就不向下传递事件。如果不能接收,事件向下传递
总结:
- iOS事件流程分为寻找响应链和响应链回溯,其中响应链回溯部分和android类似。
- 为了事件响应处理的方便,苹果又推出了UIControlEvent和UITapGestureRecognzier。它们都是针对响应链回溯过程。
- UITapGestureRecognzier和UIControlEvent不同,UIControlEvent不会响应父类的TouchBegan等操作,而UITapGestureRecognzier会响应。