ios的事件机制

引言

本文讲解主要讲解iOS事件机制相关的知识,包括事件事件,应用程序的结构,响应者对象,主事件循环,响应者链以及相关的使用。

事件(事件)

iOS版的事件主要有三种事件,

  • 触摸事件(触摸事件):iOS中最主要的就是触摸事件了,比如手指点击,手指移动以及手指离开屏幕等等。手指触摸屏幕的时候,iOS系统会生成UITouch对象来表示这次触摸,UITouch对象保存了触摸的一些信息,比如所接触的视图,触摸点的坐标,时间戳,以及触摸的时间(UITouchPhase)。有了这些触摸对象后,操作系统会生成事件对象(UIEvent),事件对象保存了之前的一系列UITouch对象,然后操作系统把这个事件对象分发到能处理这个事件的视图中(或者其他的对象,比如视图控制器,窗口,只要是应答器的子类都能响应事件),如下图所示

所有继承自UIResponder的是可以响应事件的,它有四个对应的方法

1、touchesBegan:withEvent:方法手指开始触摸的时候 2、touchesMoved:withEvent:方法手指移动的时候 3、touchesEnded:withEvent:方法手指离开的时候 4、触摸事件:与事件:外部事件到来的时候,比如电话打进来。这些方法的第一个参数是一个集合对象(集),里面保存了一系列的触摸对象,第二个参数UIEvent对象。

  • 运动事件(运动事件):比如摇一摇,系统检测到这些事件,然后发送给一个应用程序,然后由应用程序发送给第一响应者(第一响应者)。

  • 远程事件(远程事件):不好很懂,比如耳机按键控制。

    应用程序的结构

    在iOS中,视图的组织是以树状的形式组织起来的,如下图所示,一个简单的iOS的应用程序就大概有这样的结构,

整个应用程序由UIApplication的表示,应用程序管理了一个窗口对象,窗口里面管理着许多控制器,控制器又有许多的视图构成。

每一个iOS应用程序启动后都是一个独立的进程,UIApplication对象就代表一个应用程序,在iOS中,每一个应用程序有但只有一个单利形式存在的UIApplication对象,可以通[UIApplication sharedApplication]获取这个对象。应用程序的主要作用是管理应用程序的状态,以及接受和分发事件。比如下面这张图表示的,
UIApplication的可以接受用户输入事件或者系统事件,并把这些事件分发给合适的响应者对象处理,同时UIApplication的管理着系统的状态,比如应用程序启动完毕,应用程序进入后台,应用程序从后台起来或者接受通知,等等。所有这些状态的改变都由应用程序来管理,在实际的开发中,这些状态的改变会发给申请的委托,通过委托来做响应的处理。

主要事件循环

跟其他平台的应用程序一样,iOS应用程序启动之后也有一个消息循环系统,在iOS中被成为主事件循环,内部是一个运行循环,如下图所示

在主事件循环里面运行着一个运行循环,应用程序不断的获取事件,并且把事件分发给能处理这个事件的对象,处理完成之后,根据处理结果更新应用程序的状态.app不断的重复这样的循环直到关闭应用程序上图里面核心对象就是能够响应事件的对象,比如说窗口,图等等。

响应者对象和响应者链

响应者对象就是能响应事件的对象,在的UIKit中,有一个UIResponder类,这个类是所有能响应事件的类的父类,比如说视图和视图控制器,甚至是UIApplication的。

由上文的应用程序的结构可以看出iOS中所有的视图都是按照树形来组织的,每一个视图都有自己的超视图,包括viewcontroller的顶视图(也就是控制器的self.view),当一个视图被加载到superview上的时候,它的nextResponder属性就会指向它的superview,并且控制器的topmost view的nextResponder会指向所在的viewcontroller,controller的nextResponder指向窗口,Window的nextResponder指向UIApplication,这样,整个app就通过nextResponder串成一条链,就是所谓的响应链。响应链是一条虚拟的链,就是每个响应的nextResponder属性链接起来的。

命中测试视图

命中测试查看就是具体响应事件的view.window会根据不同的事件寻找这个视图,对于触摸事件,窗口最先传递给事件发生的那个视图,对于运动事件和远程事件,窗口最先把事件传递个人第一响应者事件和远程事件先不讲话,下面主要讲寻找触摸事件响应者。

寻找具体响应的过程被成为HIT-Testing.Hit-测试就相当于一个探测器,通过这个探测器可以找到手指是否点击在某个视图上面,在代码里面,这个命中测试就是一个检测方法,这个方法存在于的UIView中。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

每个手指触摸屏幕,UIApplication接受触摸的事件后,就会去调UIWindow的hitTest:withEvent:方法,看看当前的触摸的点是否在窗口内,如果在,则继续递归的调用subview的hitTest: (CGPoint)point withEvent:方法,直到找到最后视图。通过下面这张图看看它是怎么工作的。

上图的关系是,UIWindow有一个MainView,MainView有三个子视图:视图A,视图B,视图C,三个子视图分别有两个子视图,视图A,B,C的层级关系是视图A在最下面,视图B中间,视图C最上,并且视图A和视图B有一部分重叠。此时,如果手指点击在视图A和视图B的重叠部分,按照hit-test的方式,顺序如下图所示

这里的查找顺序是从窗口开始的,一开始,向根结点UIWindow发送hitTest:withEvent:消息,这个方法返回的视图就是Hit-Testing View,也就是最后能响应事件的视图当向窗口发送hitTest :withEvent:的时候,hitTest:withEvent:会检测当前的点击是否在窗口的显示范围内,如果在,则递归的调用subview的hitTest:withEvent:方法,这里调用的顺序是按照subview显示的层级来的,越在上面的子视图越早调用hitTest:withEvent :,如果点击的点不在当前的subview的返回内,则它的子视图也不会在遍历了。比如说遍历到视图C,发现点击的点不在视图C的返回内,那么就不在遍历视图C的子视图,直接遍历视图B,发现在视图B的显示返回内,则继续调用视图B的子视图的hitTest:withEvent:进行遍历,直到找到视图B.1 ,说明视图B.1就是命中测试视图直接返回到根结点,这时视图A也不会在遍历了(这也说明了被覆盖的vi EW为什么默认情况下不能响应用户的操作)下面整个过程的流程图。:

注意这里判断的时候还会检测视图的一些属性,比如userInteractionEnabled,隐藏,α,这些属性都会影响到视图是否能响应事件,如果这些不响应直接返回零(注意阿尔法的值是0.01)。同时还调了一个pointInside: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;
}

命中测试的应用

<<<<<<< HEAD

  • 扩大按钮的点击区域:

    在实际的开发过程中,可能会出现这样的需求,一个按钮设计的很小,此时需要一个更大的点击区域,有一种方法就是加一个透明的更大的按钮来响应点击事件,这样做可以实现,但是多了一个按钮对象还有另外一种方法就是通过重写则hitTest:withEvent:,在方法里面判断如果点在按钮的帧之外的某个返回内,也返回按钮自己,这样就可以实现增大点击区域的效果,(也可以使点击区域变成一个圆形)。代码如下

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
=======
* 扩大按钮的点击区域:
在实际的开发过程中,可能会出现这样的需求,一个按钮设计的很小,此时需要一个更大的点击区域,有一种方法就是加一个透明的更大的按钮来响应点击事件,这样做可以实现,但是多了一个按钮对象还有另外一种方法就是通过重写则hitTest:withEvent:,在方法里面判断如果点在按钮的帧之外的某个返回内,也返回按钮自己,这样就可以实现增大点击区域的效果,(也可以使点击区域变成一个圆形)。代码如下

-(UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {

6e21c461993c76bc026db05a16c36fa490c507e7 return nil; }

CGRect touchRect = CGRectInset(self.bounds, -80, -80);
if (CGRectContainsPoint(touchRect, point)) {
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;

<<<<<<< HEAD }

=======
}

6e21c461993c76bc026db05a16c36fa490c507e7

  • 将事件传递给兄弟视图:

    比如上图,如果视图A想要响应事件而不是视图B,不管是否重叠,在默认情况下,在重叠部分,视图A是不能响应事件的,除非让视图B的uerInteractionEnabled设为NO。如果不把userInteractionEnabled设为NO的话可以重写乙的则hitTest:withEvent:,代码如下

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hittestView = [super hitTest:point withEvent:event];
if (hittestView == self) {
return nil;
}
return hittestView;
}

事件传递

有了响应链,并且找到了第一个响应者,下面就把事件发送到这个响应者了,首先UIApplication的通过发送的SendEvent消息,把事件发送给窗口,接着窗口也发送的SendEvent消息,把事件发送给第一响应者,这个过程可以从调用堆栈中看出

当点击按钮的时候(假设第一响应者是个按钮),UIApplication的会发送开始点击和结束点击事件,分别会调到按钮的的touchesBegan和touchesEnded方法,这样就是实现了事件的传递。而事件的响应,可以看第二个图,在touchesEnded里面通过调用的UIApplication的sendAction:为:从:forEvent:。来实现如果这个按钮不响应事件,那么就会根据响应者链,把事件传给按钮的nextResponder来响应这样一级一级往上传递直到UIApplication的对象,如果应用对象还是不能响应这个事件,那么就直接抛弃这个事件。