iOS 时间响应 和 手势识别

一、事件对象

1. UIResponder 响应者对象

简介

  • 只有继承了 UIResponder 对象才能 接收处理 事件
    UIApplication、UIViewController、UIView 都继承自 UIResponder

处理事件的方法「通过覆盖以下方法实现对事件的处理」
触摸事件

  • 有 Began/ Moved/ Ended/ Cancelled 四种「针对 多根 或 一根手指」的事件方法
  • 默认的这四种触摸事件 只是将事件传递给上一个响应着「默认上一个响应者是父控件」
  • 根据 touches 中 UITouch 的个数可以判断出是单点触摸还是多点触摸
    如果两根手指同时触摸 1 个 view,那么 view 只会调用一次 touchesBegan:withEvent: 方法,touches 参数中装着 2 个UITouch 对象
    如果这两根手指一前一后分开触摸同一个 view,那么 view 会分别调用 2 次 touchesBegan:withEvent: 方法,并且每次调用时的 touches 参数中只包含 1 个 UITouch 对象
  • 部分方法示例:

// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
// Began/ Moved/ Ended/ Cancelled,4 种方法 都是 同一个 event 参数
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

// 事件 Cancelled 用于 触摸结束前某个系统事件打断触摸过程,系统会自动调用 View下面的 Cancelled 方法
// touches 存放的都是 UITouch 对象
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

加速事件

  • 有 Began/ Ended/ Cancelled 三种,例如:
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;

远程控制事件

- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

2. UITouch 对象

简介

  • 保存手指触摸的相关信息:1 个手指 对应 1 个 UITouch 对象

  • 手指触摸屏幕时,创建 1 个与手指关联的 UITouch 对象

  • 手指移动时,系统会更新对应的同 1 个 UITouch 对象

  • 手指离开屏幕时,系统会销毁相应的 UITouch 对象

注:iPhone 开发时,要避免双击事件
属性

@property(nonatomic,readonly,retain) UIWindow *window; // 触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIView *view;     // 触摸产生时所处的视图
@property(nonatomic,readonly) NSUInteger tapCount;     // 短时间内点按屏幕的次数,可用来判断单击、双击或更多的点击
@property(nonatomic,readonly) NSTimeInterval timestamp;// 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase;      // 当前触摸事件所处的状态

方法

// 返回值是 在 View 的坐标系上 当前触摸的位置「中心点的位置」 
// 调用时如果传入 View 参数为 nil,则 返回 相对于 UIWindow 的位置
- (CGPoint)locationInView:(UIView *)view;

// 记录 前一个 触摸位置 
- (CGPoint)previousLocationInView:(UIView *)view;

3. UIEvent 对象

简介

  • 每 1 个事件就会创建 1 个 UIEvent 对象
  • UIEvent 对象用于记录事件产生的 时刻类型

属性

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

UIEvent 还提供了相应的方法可以获得在某个 view 上面的触摸对象「UITouch」

4. 示例:UIView 的拖拽


// 当手指在 view 上移动的时候「在 UIView 中实现的方法」
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 获取UITouch对象
    UITouch *touch = [touches anyObject];
    
    // 获取当前点
    CGPoint curP = [touch locationInView:self];
    // 获取上一个点
    CGPoint preP = [touch previousLocationInView:self];
    
    // 获取x轴偏移量
    CGFloat offsetX = curP.x - preP.x;
    // 获取y轴偏移量
    CGFloat offsetY = curP.y - preP.y;
    
    // 修改view的位置(frame,center,transform)
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

二、找到发生事件的最合适控件

I. 事件的产生过程

  1. 系统将发生的事件加入到 UIApplication 管理的 事件队列
  2. UIApplication 取出队列的首个事件,将事件分发下去处理,「通常先发送事件给应用程序的主窗口」
  3. 主窗口在视图结构层次中 找到最适合的视图来处理

II. 找到最合适的控件的步骤「HitTest」

找到最合适的控件的步骤,也是 HitTest 的系统实现方法

  1. 视图能否 接收事件「是否继承自 UIResponder」
  2. 触摸点是否 在视图上面「pointInside 方法返回 YES」
  3. 从后往前遍历子控件,重复前面两个步骤
    <span style="color:red;">这里说的子控件指:在添加父控件类中添加的子控件,两个控件的类不一定是继承关系</span>

代码模拟

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判断当前控件能否接收事件,这里 self 指继承自 UIView 的自定义 view
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    
    // 2. 判断点在不在当前控件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    
    // 3.从后往前遍历自己的子控件
    NSInteger count = self.subviews.count;
    
    for (NSInteger 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) { // 寻找到最合适的view
            return fitView;
        }
    }
    // 循环结束,但表示没有比自己更合适的view
    return self;
}

III. 注意

主窗口在视图结构层次中,事件从 父控件 传递到 子控件「先由上往下 传递」

  • 若 父控件不能接收触摸事件,子控件更不能,则 这个父控件的父控件会处理事件

  • 若 子控件不能接收触摸事件,事件由子控件 可以接收事件的父控件 处理

  • 父控件之外的子控件,能显示,不能处理事件

  • UIView 的方法

// hitTest 的作用:用来寻找最合适的 View
// 调用时刻:当事件传递给控件的时候
// 可以通过更改此函数的返回值,来人为的选取 处理事件的控件
// point:当前的触摸点,point 这个点的坐标系就是方法调用者的坐标系
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

// 作用:判断当前触摸点在不在方法调用者「控件」上
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

三、事件的处理

响应者链条

  • 由多个响应者对象连接起来的链条
  • 能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理「默认一个事件由一个对象处理」

响应顺序

  • 若 View 的控制器存在,传递给控制器;若不存在,传给 View 的父视图
  • 视图层次结构的最顶级视图不能处理,则 传递给 Window 对象进行处理
  • 若 Window 对象不能处理,则 传递给 UIApplication 对象
  • UIApplication 对象不能处理,则 丢弃事件

I. 不接受触摸事件的情况

  1. 不接收用户交互 userInteractionEnabled = NO;
  2. 隐藏 hidden = YES;「父控件隐藏,子控件也会隐藏」
  3. 透明 alpha = 0.0 ~ 0.01「父控件透明度变化,子控件透明度也会做相应的变化」

注:UIImageView 的 userInteractionEnable 默认是 NO,因此 UIImageView默认不能接收触摸事件

II. 事件处理顺序

事件处理顺序,也是 touches 方法 默认处理顺序

  1. 调用最合适控件touchesBegan/Moved/Ended/Cancelled... 方法
    这些 touches 方法 默认将事件顺着响应链条向上传递,交给上一个响应者处理
  2. 如果最合适控件调用了 [super touchesBegan/Moved/Ended/Cancelled...];
    就会将事件顺 响应者链条 向上传递,传递给上一个响应者处理事件「由下往上 处理」
  3. 调用响应者的 touchesBegan/Moved/Ended/Cancelled... 方法

找到上一个响应者的方法

  1. 当前 View 控制器的 View,则上一个响应者为 控制器
  2. 当前 View 不是 控制器的 View,则上一个响应者为 父控件

四、手势识别

没有手势识别时,监听一个 View 上面的触摸事件

  • 自定义一个 View
  • 实现 View 的 touchesBegan/Moved/Ended/Cancelled... 方法,在方法内部实现具体处理代码

不用手势识别的缺点

  • 必须得自定义 View
  • 由于是在 View 内部的 touches 方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听 View 的触摸事件
  • 不易区分用户的具体手势行为

1. UIGestureRecognizer「抽象类」

简介

  • 为了完成手势识别,必须借助于手势识别器 UIGestureRecognizer
  • 定义了所有手势的基本行为,使用它的子类才能处理具体的手势
  • UIGestureRecognizer 的子类有:
UITapGestureRecognizer       // 敲击
UIPinchGestureRecognizer     // 捏合,用于缩放
UIPanGestureRecognizer       // 拖拽
UISwipeGestureRecognizer     // 轻扫
UIRotationGestureRecognizer  // 旋转
UILongPressGestureRecognizer // 长按

2. 手势识别的状态

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,  // 没有触摸事件发生,所有手势识别的默认状态
    UIGestureRecognizerStateBegan,     // 一个手势已经开始但尚未改变或者完成时
    UIGestureRecognizerStateChanged,   // 手势状态改变
    UIGestureRecognizerStateEnded,     // 手势完成
    UIGestureRecognizerStateCancelled, // 手势取消,恢复至Possible状态
    UIGestureRecognizerStateFailed,    // 手势失败,恢复至Possible状态
    // 识别到手势识别
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

3. 使用步骤

每一个手势识别器的用法都差不多,比如 UITapGestureRecognizer 的使用步骤如下

// 1. 创建手势识别器对象
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];

// 2. 设置手势识别器对象的具体属性
//    连续敲击 2 次
tap.numberOfTapsRequired = 2;
//    需要 2 根手指一起敲击
tap.numberOfTouchesRequired = 2;

// 3. 「如果必要」手势是可以设置代理的,要遵守 UIGestureRecognizerDelegate 协议
tap.delegate = self;

// 4. 添加手势识别器到对应的view上
[self.iconView addGestureRecognizer:tap];

// 5. 监听手势的触发
[tap addTarget:self action:@selector(tapIconView:)];

手势的常用代理方法

// 是否允许开始触发手势
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer

// 是否允许同时支持多个手势,默认是不支持多个手势「Yes 表示支持多个手势」
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer

// 是否允许接收手指的触摸点「可以控制一个 View 只有部分能够点击」
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容