事件触摸

184 阅读5分钟

1、UITouch

一个手指一次触摸屏幕,就对应生成一个 UITouch 对象,每个 UITouch 对象记录了一些触摸信息(触摸事件、位置、阶段状态、所处的视图、窗口等信息),一个触摸事件可能有多个手指触摸产生的。

2、UIEvent

UIEvent 分为不同的事件类型,一个触摸事件会对应一个 UIEvent 对象,UIEvent 对象中包含了触发该对象的触摸对象集合。

3、UIResponder

用于处理事件的 API,可以处理触摸、按压 远程控制、硬件运动事件等。不止是用来接收事件,同时也可以处理和传递对应的事件,如果当前响应着不能处理,则转发给其他合适的响应着处理。

4、事件的传递,寻找事件的第一响应着

应用程序接收到触摸事件以后,将事件加入 UIApplication 事件队列,等到处理该事件时,将该队列出队列,UIApplication 将事件传递给窗口对象 UIWindow,如果能响应时间,则从后向前遍历子视图,直到找到第一响应着

同级的视图,会先遍历后加入的子视图

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

当视图 不允许交互、隐藏、透明度<0.01 则是无法响应事件的,

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //3种状态无法响应事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    //触摸点若不在当前视图上则无法响应事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //从后往前遍历子视图数组 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) 
    { 
        // 获取子视图
        UIView *childView = self.subviews[i]; 
        // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
        CGPoint childP = [self convertPoint:point toView:childView]; 
        //询问子视图层级中的最佳响应视图
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) 
        {
            //如果子视图中有更合适的就返回
            return fitView; 
        }
    } 
    //没有在子视图中找到更合适的响应视图,那么自身就是最合适的
    return self;
}

5、事件拦截

如果子视图点击范围超出父视图,为使得点击子视图超出部分,可以响应,则需要 再父视图上进行拦截,执行下面的代码:

#import "FJFTabbar.h"

@implementation FJFTabbar

//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    //将触摸点坐标转换到在CircleButton上的坐标
    CGPoint pointTemp = [self convertPoint:point toView:self.indicateView];
    //若触摸点在CricleButton上则返回YES
    if ([self.indicateView pointInside:pointTemp withEvent:event]) {
        return YES;
    }
    //否则返回默认的操作
    return [super pointInside:point withEvent:event];
}
@end

6、事件的响应及传递

自上而下,沿着响应者链进行传递,如果拦截不向下分发事件,则需要重写:touchesBegan:withEvent: 且不调用父类的 touchesBegan:withEvent:方法,如果拦截但是继续往下分发事件 则调用父类的 touchesBegan:withEvent:方法

UIApplication 的 nextResponder 是 app delegate

7、UIGestureRecognizer、UIControl

UIGestureRecognizer、UIControl 也能够响应触摸事件,但是和 UITouch 事件又有区别

UIWindow 将事件传递给第一响应者之前,会先将事件传递给相关的手势识别器,如果手势识别器识别成功,则会取消 第一响应者对事件的响应,如果没有识别成功,则第一响应者会接收事件的处理,即 UIGestureRecognizer 比 UIResponder 具有更高的优先级

8、cancelsTouchesInView

默认为 yes,表示手势识别器成功识别了手势之后,会通知 Application 取消响应链对事件的响应,并不再传递事件给第一响应者。

9、delaysTouchesBegan

默认为no,默认情况下,手势识别器在识别手势期间,当触摸状态发生改变时,Application 会将事件传递给手势识别器和第一响应者,如果设置为yes,则不会发送给第一响应者

10、delaysTouchesEnded

默认为yes,当手势识别失败时,若此时触摸已经结束,会延迟一段时间(0.15s)再调用响应者的 touchesEnded:,若设置成 no,则会在手势识别失败时,立即通知 Application 发送状态为 end 的 touch 事件给第一响应者 以调用 touchesEnded: 结束事件响应。

11、UIControl

是 target-action 模式处理触摸事件的控件 beginTrackingWithTouch: 是在 touchesBegan: 的内部调用

系统提供有默认 action 操作的 UIControl 优先级最高,其次是 手势识别器,然后是 自定义的 UIControl,然后是 UITouch

12、 事件完整响应链

  • 用户触摸屏幕,IOKit收到屏幕操作,会将这册操作封装成IOHIDEvent对象,通过 mach port 将事件转发给 Springboard 来处理。
  • Springboard 是iOS 系统的桌面程序,Springboard收到 mach port 发过来的事件 唤醒main runloop 来处理
  • Springboard的 main runloop 将事件交给当前应用程序的 source1处理,这时会唤醒当前应用程序的 runloop,当前应用程序的 source1 会调用__IOHIDEventSystemClientQueueCallback() 函数,该函数内部会判断是否有程序在前台显示,如果有则通过 mach port 将IOHIDEvent事件转发给这个程序。如果没有,则表明 Springboard 的桌面程序没有在前台显示,函数会将事件交给 source0 处理,source0会调用该函数,函数内部做具体的处理操作
  • UIApplication 管理事件队列
  • 通过 hitTest: pointInSide 操作,去寻找第一响应者
  • 如果第一响应者 非 UIControl 子类 ,也没有手势识别器,则第一响应者具有最高优先级,因此 UIApplication 会先将事件传递给它进行处理,首先 UIApplication 通过 sendEvent: 传递给 window,window再通过 sendEvent: 传递给 第一响应者,第一响应者进行事件处理,如果不处理则传递给下一响应者,最后传递给 UIApplication,如果 UIApplication 实现了代理 ,会交给UIApplicationDelegate,如果 UIApplicationDelegate没有处理则事件会被丢弃。
  • 如果第一响应者 非 UIControl 子类 ,但是绑定了手势识别,window 会先将事件发送给手势识别器,再发送给第一响应者
  • 如果第一响应者 是自定义 UIControl 子类 ,但是绑定了手势识别,首先响应手势识别器
  • 如果第一响应者 是 UIControl 系统子类 ,但是绑定了手势识别 则先响应 target-action