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中被成为主事件循环,内部是一个运行循环,如下图所示

响应者对象和响应者链

响应者对象就是能响应事件的对象,在的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的对象,如果应用对象还是不能响应这个事件,那么就直接抛弃这个事件。

Last updated