iOS Touch 事件

242 阅读6分钟

iOS 中的事件可以分为 3 大类型

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

触摸对象(UITouch)

  • 一个手指一次触摸屏幕,就对应生成一个 UITouch 对象。多个手指同时触摸,生成多个UITouch对象。

  • 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个 UITouch 对象。

    1. 若两个手指一前一后触摸同一个位置(双击), 那么第一次触摸时生成一个 UITouch 对象,第二次触摸更新这个 UITouch 对象( UITouch 对象的 tapCount 属性值从1变成2);
    2. 若两个手指一前一后触摸的位置不同,将会生成两个 UITouch 对象,两者之间没有联系。
  • 每个 UITouch 对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。

  • 手指离开屏幕一段时间后,确定该 UITouch 对象不会再被更新将被释放。

事件对象(UIEvent)

  • 触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个 UIEvent 对象,其中的 type 属性标识了事件的类型(事件不只是触摸事件)。
  • UIEvent 对象中包含了触发该事件的触摸对象的集合,因为一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过 allTouches 属性获取。

响应者对象(UIResponder)

只有继承了 UIResponder 的对象才能接收、处理、传递对应的事件。我们称之为“响应者对象”。

UIApplication、UIViewController、UIView 都继承自 UIResponder,因此它们都是响应者对象,

UIResponder 内部提供了以下方法来处理事件:

//触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

//加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

//远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

事件的处理

UIView 的触摸事件处理

⾸先第⼀步要自定义 UIView, 因为只有实现了 UIResponder 的事件⽅方法才能够监听事件.

//当一根或者多根手指开始触摸view,系统自动调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}
//当一根或者多根手指在view上移动时,系统自动调用
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}
//当⼀根或者多根手指离开view时,系统⾃动调用
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}
//当系统事件打断当前的触摸事件时,系统会⾃动调用
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}

UIView 的拖拽

  1. ⾃定义UIView,实现监听方法.
  2. 在 TouchMoved 方法当中进⾏操作,因为用户手指在视图上移动的时候才需要移动视图。
  3. 获取当前手指的位置和上⼀个⼿指的位置.
  4. 当前视图的位置 = 上一次视图的位置 - ⼿指的偏移量
//当触摸的位置发生变化时,调用此方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //1.获取手指的对象
    UITouch *touch = [touches anyObject];
    //2.获取当前手指所在的点.
    CGPoint loc = [touch locationInView:self];
    //3.获取手指的上一个点.
    CGPoint preLoc = [touch previousLocationInView:self];
    //X轴⽅方向偏移量
    CGFloat offsetX = loc.x - preLoc.x;
    //Y轴⽅方向偏移量
    CGFloat offsetY = loc.y - preLoc.y;
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

事件的传递和响应

事件的传递

因为最佳响应者具有最高的事件响应优先级,因此 UIApplication 会先将事件传递给它供其响应。首先,UIApplication 将事件通过 sendEvent: 传递给事件所属的 window,window 同样通过 sendEvent: 再将事件传递给最佳响应者。

假如应用中存在多个 window 对象,UIApplication 同样可以通过 sendEvent: 将事件传递给事件所属的 window,因为sendEvent:方法的参数 event 有属性 allTouches 而 UITouch 对象 有属性window 和 view 等 。

事件的传递的过程
  1. 发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中。
  2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(如果存在多个窗口,则优先询问后显示的窗口)。
  3. 窗口会在视图层次结构中自下而上传递找到事件最佳响应者(最合适的视图)。
寻找事件的最佳响应者(Hit-Testing)
  1. 自己是否能接收触摸事件?(如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件)

    UIView不接收触摸事件的三种情况:

    1. 不接收用户交互 userInteractionEnabled = NO
    2. 隐藏 hidden = YES
    3. 透明 alpha = 0.0 ~ 0.01
  2. 触摸点是否在自己身上

  3. 从后往前遍历子控件,重复前面的两个步骤,之所以会采取从后往前遍历,是因为后添加的 view 在上面,降低循环次数。

  4. 如果没有符合条件的子控件,那么就自己最适合的控件

hitTest:WithEvent: 方法本质

只要事件一传递给一个控件,那么该控件就会调用hitTest:WithEvent:方法, 寻找并返回最合适的view。

依据以上的描述我们可以推测出hitTest:WithEvent:的默认实现大致如下

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //1.判断⾃己能不能接收事件
    if (NO == self.userInteractionEnabled || YES == self.hidden || 0.01 >= self.alpha) return nil;
    
    //2.判断点在不在自己身上
    if (![self pointInside:point withEvent:event]) return nil;
    
    //3.从后往前遍历子控件,把事件传递给子控件,调用子控件的hitTest,
    NSUInteger count = self.subviews.count;
    for (NSUInteger i = count - 1; i >= 0; i--) {
        
        //3.1获取子控件
        UIView *childView = self.subviews[i];
        
        //3.2把当前点的坐标系转换成子控件的坐标系
        CGPoint childPoint = [self convertPoint:point toView:childView];
        
        //3.3子控件调用hitTest
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        //最适合的View如果有就返回
        if (fitView) return fitView;
    }
    //4.如果子控件没有找到最适合的View,那么自己就是最适合的View.
    return self;
}

事件的响应

如果最佳响应者未重写相关的触摸事件方法截获事件进行自定义的响应操作,则 touchesBegan 方法的默认做法是将事件顺着响应者链条向下传递

判断下一个响应者:

  1. 若视图是控制器的根视图,则其 nextResponder 为控制器对象;否则,其 nextResponder 为父视图。

  2. 若控制器的视图是 window 的根视图,则其 nextResponder 为窗口对象;若控制器是从别的控制器 present 出来的,则其nextResponder 为 presenting view controller。

  3. 若是 UIWindow 则 nextResponder 为 UIApplication 对象。

  4. 若当前应用的 appDelegate 是一个 UIResponder 对象,且不是 UIView、UIViewController 或 app 本身,则 UIApplication 的nextResponder 为 appDelegate。

  5. 如果 appDelegate 不处理该事件,则将其丢弃