iOS事件分发机制与实践

iOS事件的传递与响应是一个重要的话题,网上谈论的不少,但大多讲述并不完整,本文将结合苹果官方的文档对事件的传递与响应原理及应用实践作一个比较完整的总结。文章将依次介绍下列内容:bash

  • 事件的传递机制
  • 事件的响应机制
  • 事件传递与响应实践
  • 手势识别器工做机制
  • 标准控件的事件处理

iOS中事件一共有四种类型,包含触摸事件,运动事件,远程控制事件,按压事件,本文将只讨论最经常使用的触摸事件。事件经过UIEvent对象描述app

UIEvent

UIEvent描述了单次的用户与应用的交互行为,例如触摸屏幕会产生触摸事件,晃动手机会产生运动事件。UIEvent对象中记录了事件发生的时间,类型,对于触摸事件,还记录了一组UITouch对象,下面是UIEvent的几个属性:ide

@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);  //事件的类型
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval  timestamp;  //事件的时间

@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;  //事件包含的touch对象
复制代码

那么触摸事件中的UITouch对象描述的是什么呢?ui

UITouch

UITouch记录了手指在屏幕上触摸时产生的一组信息,包含触摸的时间,位置,所在的窗口或视图,触摸的状态,力度等信息atom

@property(nonatomic,readonly) NSTimeInterval      timestamp;  //时间
@property(nonatomic,readonly) UITouchPhase        phase;  //状态,例如begin,move,end,cancel
@property(nonatomic,readonly) NSUInteger          tapCount;   // 短期内单击的次数
@property(nonatomic,readonly) UITouchType         type NS_AVAILABLE_IOS(9_0);  //类型
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);  //触摸半径
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;  //触摸所在窗口
@property(nullable,nonatomic,readonly,strong) UIView                          *view;  //触摸所在视图
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);  //正在接收该触摸对象的手势识别器
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);  //触摸的力度
复制代码

每一根手指的触摸都会产生一个UITouch对象,多个手指触摸便会有多个UITouch对象,当手指在屏幕上移动时,系统会更新UITouch的部分属性值,在触摸结束后系统会释放UITouch对象。spa

当事件产生后,系统会寻找能够响应该事件的对象来处理事件,若是找不到能够响应的对象,事件就会被丢弃。那么哪些对象能够响应事件呢?只有继承于UIResponder的对象才可以响应事件,UIApplication,UIView,UIViewcontroller均继承于UIResponder,所以它们均可以响应事件。UIResponder提供了响应事件的一组方法:code

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;  //手指触摸到屏幕
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指在屏幕上移动或按压
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指离开屏幕
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //触摸被中断,例如触摸时电话呼入
复制代码

若是咱们想要对事件进行自定义的处理(好比手指在屏幕滑动时让某个view跟着移动),咱们须要重写以上四个方法,对于UIViewcontroller,咱们只须要在UIViewcontroller中重写上面四个方法,对于UIView,咱们须要建立继承于UIView的子类,而后在子类中重写上面的方法,这点须要注意cdn

事件的传递

事件产生以后,会被加入到由UIApplication管理的事件队列里,接下来开始自UIApplication往下传递,首先会传递给主window,而后按照view的层级结构一层层往下传递,一直找到最合适的view(发生touch的那个view)来处理事件。查找最合适的view的过程是一个递归的过程,其中涉及到两个重要的方法 hitTest:withEvent:pointInside:withEvent:对象

当事件传递给某个view以后,会调用view的hitTest:withEvent:方法,该方法会递归查找view的全部子view,其中是否有最合适的view来处理事件,整个流程以下所示:blog

hitTest工做流程

hitTest:withEvent:代码实现:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //首先判断是否能够接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    //而后判断点是否在当前视图上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    //循环遍历全部子视图,查找是否有最合适的视图
    for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        //转换点到子视图坐标系上
        CGPoint childPoint = [self convertPoint:point toView:childView];
        //递归查找是否存在最合适的view
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        //若是返回非空,说明子视图中找到了最合适的view,那么返回它
        if (fitView) {
            return fitView;
        }
    }
    //循环结束,仍旧没有合适的子视图能够处理事件,那么就认为本身是最合适的view
    return self;
}
复制代码
  • pointInside:withEvent:方法做用是判断点是否在视图内,是则返回YES,不然返回NO
  • 判断一个view是否可以接收事件有三个条件,分别是,是否禁止用户交互(userInteractionEnabled = NO),是否被隐藏(hidden = YES)以及透明度是否小于等于0.01(alpha <=0.01)
  • 从递归的逻辑咱们知道,若是触摸的点不在父view上,那么其上的全部子view的hitTest都不会被调用,须要指出的是,若是子view尺寸超出了父view,而且属性clipsToBounds设置为NO(也就是子view超出部分不被裁剪),触摸发生在子view超出父view的区域内,依旧不返回子view。反过来,若是触摸的点在父view上而且父view就是最合适的view,那么它的全部子view的hitTest仍是会被调用,由于若是不调用就没法知道是否还有比父view更合适的子view存在。

事件的响应

在找到最合适的view以后,会调用view的touches方法对事件进行响应,若是没有重写view的touches方法,touches默认的作法是将事件沿着响应者链往上抛,交给下一个响应者对象。也就是说,touches方法默认不处理事件,只是将事件沿着响应者链往上传递。那么响应者链是什么呢?

响应者链

在应用程序中,视图放置都是有必定层次关系的,点击屏幕以后该由下方的哪一个view来响应须要有一个判断的方式。响应者链是由一系列能够响应事件的对象(继承于UIResponder)组成的,它决定了响应者对象响应事件的前后顺序关系。下图展现了UIApplication,UIViewcontroller以及UIView之间的响应关系链:

响应者链

响应者链在递归查找最合适的view的时候造成,所找到的view将成为第一响应者,会调用它的touches方法来响应事件,touches方法默认的处理是将事件往上抛给下一个响应者,而若是下一个响应者的touches方法没有重写,事件会继续沿着响应者链往上走,一直到UIApplication,若是依旧不能处理事件那么事件就被丢弃。

  • UIView

    若是view是viewcontroller的根view,那么下一个响应者是viewcontroller,不然是super view

  • UIViewcontroller

    若是viewcontroller的view是window的根view,那么下一个响应者是window;若是viewcontroller是另外一个viewcontroller模态推出的,那么下一个响应者是另外一个viewcontroller;若是viewcontroller的view被add到另外一个viewcontroller的根view上,那么下一个响应者是另外一个viewcontroller的根view

  • UIWindow

    UIWindow的下一个响应者是UIApplication

  • UIApplication

    一般UIApplication是响应者链的顶端(若是app delegate也继承了UIResponder,事件还会继续传给app delegate)

事件传递与响应实践

首先咱们经过代码建立一个具备层次结构的视图集合,在viewcontroller的viewDidLoad中添加以下代码:

greenView *green = [[greenView alloc] initWithFrame:CGRectMake(50, 50, 300, 500)];
    [self.view addSubview:green];
    
    redView *red = [[redView alloc] initWithFrame:CGRectMake(0, 0, 200, 300)];
    [green addSubview:red];
    
    orangeView *orange = [[orangeView alloc] initWithFrame:CGRectMake(0, 350, 200, 100)];
    [green addSubview:orange];
    
    blueView *blue = [[blueView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
    [red addSubview:blue];
复制代码

执行后以下所示:

视图

要实现咱们自定义的事件处理逻辑,一般有两种方式,咱们能够重写hitTest:withEvent:方法指定最合适处理事件的视图,即响应链的第一响应者,也能够经过重写touches方法来决定该由响应链上的谁来响应事件。

  • 情景1:点击黄色视图,红色视图响应

黄色视图和红色视图均为绿色视图的子视图,咱们能够重写绿色视图的hitTest:withEvent:方法,在其中直接返回红色视图,代码示例以下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    if ([self pointInside:point withEvent:event] == NO) return nil;
    //红色视图是先被add的,因此是第一个元素
    return self.subviews[0];
}
复制代码

咱们这里是重写了父视图的hitTest方法,而不是重写红色视图的hitTest方法并让它返回自身,道理也很显然,在遍历绿色视图全部子视图的过程当中,可能还没来得及调用到红色视图的hitTest方法时,就已经遍历到了触摸点真正所在的黄色视图,这个时候重写红色视图的hitTest方法是无效的。

  • 情景2:点击红色视图,绿色视图响应(也就是事件透传)

咱们能够重写红色视图的hitTest方法,让其返回空,这时候便没有了合适的子视图来响应事件,父视图即绿色视图就成为了最合适的响应事件的视图,代码示例以下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return nil;
}
复制代码

固然,咱们也能够重写绿色视图的hitTest方法,让其直接返回自身,也能实现一样效果,不过这样的话点击其它子视图(好比黄色视图)就也不能响应事件了,所以如何处理须要视状况而定。

  • 情景3:点击红色视图,红色和绿色视图均作响应

咱们知道,事件在不能被处理时,会沿着响应者链传递给下一个响应者,所以咱们能够重写响应者对象的touches方法来实现让一个事件多个响应者对象响应的目的。所以咱们能够经过重写红色视图的touches方法,先作本身的处理,而后在把事件传递给下一个响应者,代码示例以下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches begin");  //本身的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches moved");  //本身的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches end");  //本身的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches canceled");  //本身的处理
    [super touchesBegan:touches withEvent:event];
}
复制代码

须要说明的是,事件传递给下一个响应者时,用的是super而不是superview,这并无问题,由于super调用了父类的实现,而父类默认的实现就是调用下一个响应者的touches方法。若是直接调用superview反而会有问题,由于下一个响应者多是viewcontroller

手势识别器

事实上,咱们要处理事件除了使用前面提到的方式,还有另外一种方式,就是手势识别器。手势识别器能够很方便的处理经常使用的各类触摸事件,常见的手势包括单击、拖动,长按,横扫或竖扫,缩放,旋转等,另外咱们还能够建立自定义的手势。

UIGestureRecognize是手势识别器的父类,全部具体的手势识别器均继承于该父类,若是咱们自定义手势,也须要继承该类。然而,该类并无继承于UIResponder,因此手势识别器并不参与响应者链。那么手势识别器是如何工做的呢?

手势识别器工做机制

当触摸屏幕产生touch事件后,UIApplication会将事件往下分发,若是视图绑定了手势识别器,那么touch事件会优先传递给绑定在视图上的手势识别器,而后手势识别器会对手势进行识别,若是识别出了手势,就会调用建立手势时所绑定的回调方法,而且会取消将touch事件继续传递给其所绑定的视图,若是手势识别器没有识别出对应的手势,那么touch事件会继续向手势识别器所绑定的视图传递。

虽然手势识别器并非响应者链中的一员,可是手势识别器像一个观察者,会在一旁观察touch事件,并延迟事件向所绑定的视图传递,这短暂的延迟使手势识别器有机会优先去识别手势处理touch事件。

标准控件的事件处理

对于UIKit提供的的标准控件,能够很方便地经过Target-Action的方式增长事件处理逻辑(例如UIButton的addTarget方法),那么Target-Action,手势识别器,以及touches方法的优先顺序是怎样的呢?

  • 情景1

    咱们以UIbutton为例,首先继承UIbutton并重写touches方法,而后建立button对象并绑定单击手势,而后再经过addtarget的方式添加点击事件。三者同时存在时,手势识别器优先响应,其余方式再也不响应,手势识别器不存在时,touches方法优先响应,仅当UIbutton没有绑定手势识别器,也没有被重写touches方法时,target-action方式才会响应。这里咱们也能够推测target-action方式应该就是重写了button的touches方法

  • 情景2

    仍以UIbutton为例,咱们建立button对象,并在button的父视图上绑定手势(或者重写父视图的touches方法),结果是button的target-action方式优先进行了响应,父视图并无响应。这也很显然,从hittest的递归逻辑看,当发现了合适的子视图(button)时就直接由子视图第一响应,父视图将不是最合适的响应者,固然它处于响应者链的上一层。

相关文章
相关标签/搜索