iOS UITouch事件处理-原理篇

前言:

事件 是界面交互(或人机交互)的最基本组成之一。没有它手机上的App就失去了存在的意义。
一个事件的周期:事件的产生——事件的传递——事件的响应
事件传递到响应实现原理:hitTest:withEvent: 和pointInside:withEvent 和touches方法

事件传递和事件响应区别:

事件的传递是自上而下(父控件到子控件);事件的响应是自下而上(顺着响应者链条向上传递:子控件到父控件。

iOS中的事件类型

  • 触摸事件(touch events)
  • 按压事件(press events)
  • 摇晃事件(shake - motions events)
  • 远程控制事件 (remote - controls events)
  • 编辑菜单消息事件(editing menu messages)

一、事件的产生和传递

  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
    找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

1.1 事件传递原理

事件传递是通过下面两个方法完成的:

  1. hitTest:withEvent: (UIView 的实例方法)
  2. pointInside:withEvent (UIView 的事例方法)
传递过程:
  1. 主窗口接收到应用传过来的事件后,首先判断能不能接收事件。如果能,就会判> 断触摸点在不在自身范围内。(通过pointInside:withEvent判断)如果不在,就不处理。如果在就进行第二步。
  2. 如果触摸点在自己的坐标范围内,那么窗口会从后往前遍历自己的子控件,来寻找最合适的view。(通过hitTest:withEvent:方法递归寻找)
  3. 遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
  4. 按照上面步骤循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就是最合适的view。
UIView不能接收事件的三种情况:
1. 不允许交互 `userInteractionEnabled` = NO
2. 透明度 `alpha` < 0.01
3. 父视图或者子视图的 `hidden` = YES

1.2 hitTest:withEvent:代码具体实现

// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// event:触摸事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
   // 判断自己能否接收事件
    if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
        return nil;
    }
    
    // 判断点是不是在当前视图上
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    
    // 从后往前遍历自己的子控件,寻找更合适的View
    for (long i = self.subviews.count - 1; i >= 0; i--) {
        // 获取子控件
        UIView *childView = self.subviews[i];
        
        // 将自己坐标系的点转化成子控件坐标系的点
        CGPoint childPoint = [self convertPoint:point toView:childView];
        
        // 递归调用hitTest方法,寻找更加合适的View
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    
    // 没有找到比自己更适合的View
    return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
     return NO;
}

1.3 重写 hitTest:withEvent: 方法实用之处

1.3.1 拦截事件的处理
  • 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
  • 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
  • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
栗子:
//BView是一个view的子视图
//BView是UIButton的父视图,但是BView的userInteractionEnable= NO
//通过hitTest可以拦截事件的功能 就可以指定UIButton作为响应者。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    for (UIView *v  in self.subviews) {
        if ([v isKindOfClass:NSClassFromString(@"BView")]) {
            for (UIView *vv  in v.subviews) {
                if ([vv isKindOfClass:NSClassFromString(@"UIButton")]) {
                    CGPoint insideP = [self convertPoint:point toView:vv];
                    BOOL isInsidePoint =  [vv pointInside:insideP withEvent:event];
                    if (isInsidePoint) {
                        return vv;
                        break;
                    }
                }
            }
            break;
        }
    }
    return self;
}
1.3.2 return nil作用
return nil的含义:

hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。

特殊用法

谁都不能处理事件,窗口也不能处理。
重写window的hitTest:withEvent:方法return nil
只能有窗口处理事件。

控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self

二、事件的响应

2.1响应者对象(UIResponder)

iOS中,我们最常接触的事件是触摸事件,什么样的对象才能处理触摸事件呢?

只有继承UIResponder的类或者子类才可以响应和处理事件。因为UIResponder:是用于响应和处理事件的抽象对象。

具体可以处理触摸事件的类如下:

  • UIApplication
  • UIViewController (包括它的子类)
  • UIView (包括它的子类)

2.2 下面是UIResponder类提供的响应和处理触摸事件的4方法:

//开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

通过这4个方法就可以知道触摸事件的整个过程。四个方法中的两个参数UITouch 和 UIEvent 解释下:

UITouch对象
  • 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
  • 一根手指对应一个UITouch对象
UITouch作用
  • 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
  • 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触 摸位置
  • 当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的主要属性和方法
//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
 
//触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
 
//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
 
//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
 
//当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;

//获取手指与屏幕的接触半径 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);

//获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

//获取触摸手势
@property(nullable,nonatomic,readonly,copy)   NSArray  *gestureRecognizers NS_AVAILABLE_IOS(3_2);

//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);

//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);

//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;

//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
UIEvent 对象

每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型

主要属性和方法

 //事件类型
 @property(nonatomic,readonly) UIEventType    type;

//事件子类型
 @property(nonatomic,readonly) UIEventSubtype  subtype;

//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval  timestamp;

//返回值:返回与接收器相关联的所有触摸对象。
- (nullable NSSet  *)allTouches;
 
// 返回值:返回属于一个给定视图的触摸对象,用于表示由接收器所表示的事件。
- (nullable NSSet  *)touchesForView:(UIView *)view;
 
//返回值:返回属于一个给定窗口的接收器的事件响应的触摸对象。
- (nullable NSSet  *)touchesForWindow:(UIWindow *)window;

//返回值:返回触摸对象被传递到特殊手势识别
- (nullable NSSet  *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture 

终于说完了UITouch 和 UIEvent ,其实还有很多细节,这里就不展开说了,回到touches的四个方法上来说事情:

  1. touches 里包含了一个或多UITouch对象,也即一个或多个手指同时触摸view,因此touches.count就是触摸的点数,是1就是单点触摸,大于1就是多点触摸。
  2. 一次触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
    如果两根手指同时触摸一个view,那就是一个事件,view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
  3. 如果这两根手指一前一后分开触摸同一个view,那就是两个事件,view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

响应者链传递过程示意图:

iOS UITouch事件处理-原理篇_第1张图片
响应者链视图.png

以上四个方法就是view用来响应事件然后做出处理的过程。

注意事项:
  • 在view上使用这四个方法,必须自定义view然后重写父类方法。
  • 在viewController里,重写这四个方法,是响应viewController的事件,viewController上的子视图需要响应和处理事件需要自定义,不要搞混淆了,以为在viewController里面写,就是重写了view的这四个方法。
  • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
view重写touches方法,处理事件的缺陷
1. 必须自定义View
2. 由于是View内部的touches方法中监听触摸事件,因此默认情况下无法让其他外界对象监听View的触摸事件
3. 不容易区分用户的具体手势行为。
使用技巧:

如何做到一个事件多个对象处理?
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 1.自己先处理事件...
    NSLog(@"do somthing...");
   // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
   [super touchesBegan:touches withEvent:event]; 
}

关于iOS-UIGestureRecognizer请看 iOS-手势详解
本文借鉴了一些前辈的文章,如果有不对的地方请指正,欢迎大家一起交流学习。

你可能感兴趣的:(iOS UITouch事件处理-原理篇)