iOS开发之UI篇(4)—— 触摸事件

版本

Xcode 9.1

UIApplication、UIViewController、UIView均继承自UIResponder。
在iOS中,只有继承了UIResponder的对象才能接收处理事件,此对象即为事件“响应者”。
接下来从响应方法、触摸对象UITouch、事件对象UIEvent和响应链这四个方面进一步分析。然后写个示例,也就算了。

1. 响应方法

UIResponder中提供了五个响应方法来处理触摸事件:

// 一根或多根手指触摸开始
- (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;
 当无法获得touches真实值的时候调用该方法(iOS9.1开始引入)
 当UIKit无法得到准确的touches值的时候(例如触摸笔处在屏幕边缘,此时无法获得触摸笔的高度和定位),
 会生成此touches的预估值,导致estimatedPropertiesExpectingUpdates这个属性发生改变,
 当属性estimatedPropertiesExpectingUpdates发生改变(不为空或者更新)时,调用该方法
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

2. 触摸对象UITouch

当手指触摸屏幕时,会创建一个与手指相关的UITouch对象,且一根手指对应一个对象。
如果多根手指同时触摸屏幕,那么会依次创建多个对象,这些对象会被包含在响应方法的形参touches里面。
UITouch中包含的常用信息有:

window属性:触摸时所在的窗口
view属性:触摸时所在视图
tapCount属性:短时间内点击的次数(可判断单点还是多点)
timestamp属性:触摸产生或变化的时间戳
phase属性:触摸周期内的各个状态
locationInView:方法:取得在指定视图的位置
previousLocationInView:方法:取得移动的前一个位置

3. 事件对象UIEvent

当发生一个事件时,就会对应产生一个UIEvent对象。
在一次完整的触摸过程中,只会产生一个UIEvent,也就是说,五个响应方法都是同一个UIEvent对象(即响应方法的形参event)。
UIEvent中包含的常用信息有:

type属性:事件类型
timestamp属性:事件产生的时间
touchesForWindow:方法:返回当前窗口上的触摸对象UITouch
touchesForView:方法:返回特定视图上的触摸对象UITouch

4. 响应链

前面讲到UIApplication、UIViewController、UIView均能响应触摸事件,当有多个view重叠的时候触摸事件是怎样响应的呢?

  • 当一个事件发生后首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);
  • 如果上级视图(黄色view)仍然无法处理则会继续往上传递;
  • 一直传递到视图控制器view controller,首先判断视图控制器的根视图(蓝色view)是否能处理此事件;
  • 如果不能则接着判断该视图控制器view controller能否处理此事件,如果还是不能则继续向上传递;
  • 一直到window,如果window还是不能处理此事件则继续交给application(UIApplication单例对象)处理;
  • 如果最后application还是不能处理此事件则将其丢弃。
  • 在这个过程中各个对象如何知道自己能不能处理该事件呢?对于继承UIResponder的对象,其不能处理事件有几个条件:

  • userInteractionEnabled = NO(其子视图也将无法响应事件)
  • hidden = YES
  • alpha = 0~0.01
  • 没有实现开始触摸方法(touchesBegan:withEvent:)
  • 注:前三点都是针对UIView控件或其子控件而言的

    在ViewController的子视图(self.view)上面依次添加三个view:GreenView 、BlueView 和RedView。ViewController和这三个view均实现触摸方法,其中在三个view的触摸移动方法中均设置该view中心点跟随触摸点移动(这样我们触摸移动哪个view时,这个view就会跟随触摸点移动),在ViewController的触摸移动方法中均设置背景颜色随机变化。下面结合效果图来解析:

  • 触摸RedView时,由于RedView中设置了userInteractionEnabled = No,使得RedView并没有响应事件(相对于父视图移动),而是将事件传递到父视图(BlueView),由父视图响应事件;
  • 触摸BlueView 时,BlueView响应了触摸事件(相对于父视图移动),没有将事件传递给它的父视图(GreenView),所以拖动BlueView时,GreenView并没有动;
  • 触摸GreenView时,GreenView响应了触摸事件(相对于父视图移动),由于在GreenView的touchesBegan方法里将事件传递给下一个响应者,使得ViewController也响应了事件(背景颜色随机改变)。
  • - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"RedView:touchesBegan"); // 触摸移动中(随着手指的移动,会多次调用该方法) - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"RedView:touchesMoved"); /* 设置当前view随手指移动 */ // 取得一个触摸对象(对于多点触摸可能有多个对象) UITouch *touch = [touches anyObject]; // 取得触摸点在当前视图中的位置 CGPoint current = [touch locationInView:self]; // 取得前一个触摸点位置 CGPoint previous = [touch previousLocationInView:self]; // 触摸点移动偏移量 CGPoint offset = CGPointMake(current.x-previous.x, current.y-previous.y); // 当前视图移动前的中点位置(相对于父视图) CGPoint center = self.center; // 重新设置当前视图新位置 self.center = CGPointMake(center.x+offset.x, center.y+offset.y); // 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"RedView:touchesEnded"); // 触摸被取消(触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程) - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"RedView:touchesCancelled"); // 当无法获得touches真实值的时候调用该方法(一般使用触摸笔才出现此状况) - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches { NSLog(@"RedView:touchesEstimatedPropertiesUpdated");

    BlueView.m

    #pragma mark - 触摸事件响应方法
    // 触摸开始
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"BlueView:touchesBegan");
    // 触摸移动中(随着手指的移动,会多次调用该方法)
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"BlueView:touchesMoved");
        /* 设置当前view随手指移动 */
        // 取得一个触摸对象(对于多点触摸可能有多个对象)
        UITouch *touch = [touches anyObject];
        // 取得触摸点在当前视图中的位置
        CGPoint current = [touch locationInView:self];
        // 取得前一个触摸点位置
        CGPoint previous = [touch previousLocationInView:self];
        // 触摸点移动偏移量
        CGPoint offset = CGPointMake(current.x-previous.x, current.y-previous.y);
        // 当前视图移动前的中点位置(相对于父视图)
        CGPoint center = self.center;
        // 重新设置当前视图新位置
        self.center = CGPointMake(center.x+offset.x, center.y+offset.y);
    // 触摸结束
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"BlueView:touchesEnded");
    // 触摸被取消(触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程)
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"BlueView:touchesCancelled");
    // 当无法获得touches真实值的时候调用该方法(一般使用触摸笔才出现此状况)
    - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches {
        NSLog(@"BlueView:touchesEstimatedPropertiesUpdated");
    

    GreenView.m

    #pragma mark - 触摸事件响应方法
    // 触摸开始
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"GreenView:touchesBegan");
        // 传递触摸事件给下一响应者
        [super touchesBegan:touches withEvent:event];
        // 用[self nextResponder]传递过去的事件只响应touchesBegan,而不响应touchesMoved、touchesEnded等
    //    [[self nextResponder] touchesBegan:touches withEvent:event];
    // 触摸移动中(随着手指的移动,会多次调用该方法)
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"GreenView:touchesMoved");
        /* 设置当前view随手指移动 */
        // 取得一个触摸对象(对于多点触摸可能有多个对象)
        UITouch *touch = [touches anyObject];
        // 取得触摸点在当前视图中的位置
        CGPoint current = [touch locationInView:self];
        // 取得前一个触摸点位置
        CGPoint previous = [touch previousLocationInView:self];
        // 触摸点移动偏移量
        CGPoint offset = CGPointMake(current.x-previous.x, current.y-previous.y);
        // 当前视图移动前的中点位置(相对于父视图)
        CGPoint center = self.center;
        // 重新设置当前视图新位置
        self.center = CGPointMake(center.x+offset.x, center.y+offset.y);
    // 触摸结束
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"GreenView:touchesEnded");
    // 触摸被取消(触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程)
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"GreenView:touchesCancelled");
    // 当无法获得touches真实值的时候调用该方法(一般使用触摸笔才出现此状况)
    - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches {
        NSLog(@"GreenView:touchesEstimatedPropertiesUpdated");
    

    ViewController.m

    #pragma mark - 触摸事件响应方法
    // 触摸开始
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"ViewController:touchesBegan");
    // 触摸移动中(随着手指的移动,会多次调用该方法)
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"ViewController:touchesMoved");
        // 设置背景随机颜色
        CGFloat red = (CGFloat)random() / (CGFloat)RAND_MAX;
        CGFloat green = (CGFloat)random() / (CGFloat)RAND_MAX;
        CGFloat blue = (CGFloat)random() / (CGFloat)RAND_MAX;
        self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
    // 触摸结束
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"ViewController:touchesEnded");
    // 触摸被取消(触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程)
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"ViewController:touchesCancelled");
     当无法获得touches真实值的时候调用该方法(一般使用触摸笔才出现此状况)
     当UIKit无法得到准确的touches值的时候(例如触摸笔处在屏幕边缘,此时无法获得触摸笔的高度和定位),
     会生成此touches的预估值,导致estimatedPropertiesExpectingUpdates这个属性发生改变,
     当属性estimatedPropertiesExpectingUpdates发生改变(不为空或者更新)时,调用该方法
     @param touches 触摸对象