iOS 点击事件传递及响应

作者: 苦工 | 来源:发表于2016-08-26 18:18 被阅读14182次
前言:苹果的官方文档《Event Handling Guide for iOS》对事件处理做了非常详尽清晰的解释,建议大家仔细研读

关于iOS的事件响应机制网上讲解文章不少,有的文章内容少没讲全面,有的说的太多,一个概念反复说,传递和响应混在一起讲,不好理解,我综合参考了几篇文章总结了一下,觉得可以分为以下几点来讲

1. iOS中的事件介绍
2. 事件的产生和传递
3. 事件响应
4. 实际项目中的应用

1.iOS中的事件介绍

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

  • 触屏事件(例如点击按钮、通过手势缩放图片、拖动上下滚动页面等)
  • 传感器事件(例如摇一摇红包、通过旋转设备控制赛车方向、指南针等)
  • 远程控制事件(例如耳机的线控、外接手柄、遥控器等)

着重讲解一下iOS处理触屏事件,触屏事件分为两种方式:

  • 高级事件处理:利用UIKit提供的各种用户控件或者手势识别器来处理事件。
  • 低级事件处理:在UIView的子类中重写触屏回调方法,直接处理触屏事件。

如果想监听一个view上面的触摸事件,之前的做法是:

(1)自定义一个view。

(2)实现view的touches方法,在方法内部实现具体处理代码。

通过touches方法监听view触摸事件,有很明显的几个缺点:

(1)必须得自定义view。

(2)由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件。

(3)不容易区分用户的具体手势行为。

iOS 3.2之后,苹果推出了手势识别功能(Gesture Recognizer),在触摸事件处理方面,大大简化了开发者的开发难度。

UIKit中我们常用的是UIControl类实例的addTarget:action:forControlEvents:方法维护控件目标行为表,除了UIKit控件外,手势识别器UIGestureRecognizer类的实例也是处理触屏事件的好帮手,其内部也使用目标行为表。

为什么手势和单击事件只会响应手势?

UIGestureRecognizer 有个属性cancelsTouchesInView,这个属性默认值是YES,即当手势识别成功后,会发送touchesCancelled消息给view来结束view的响应。
如果cancelsTouchesInView为NO,那么gestureRecognizer和view都可以响应

UIKit内置了6种手势识别器:

UITapGestureRecognizer:点击(单击、双击、三连击等)手势。
UIPinchGestureRecognizer:缩放手势。
UIPanGestureRecognizer:拖拽手势。
UISwipeGestureRecognizer:滑动手势。
UIRotationGestureRecognizer:旋转手势。
UILongPressGestureRecognizer:长按手势。

UIKit控件和手势识别器属于高级事件处理的范畴,这些不再多说,以下文字都是介绍的低级事件处理过程

#### UITouch

当你用一根手指触摸屏幕时, 会创建一个与之关联的UITouch对象, 一个UITouch对象对应一根手指. 在事件中可以根据NSSet中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;


#### UIEvent

每产生一个事件, 就对应产生一个UIEvent. UIEvent记录着该事件产生的时间, 事件的类型等等

UIEvent几个重要的属性 :

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



#### 响应者对象(UIResponder)
在iOS中不是任何对象都能处理事件, 只有继承了UIResponder的对象才能接收并处理事件,我们称为响应者对象
UIApplication,UIViewController,UIView都继承自UIResponder,因此他们都是响应者对象, 都能够接收并处理事件

继承自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相关属性:

* multipleTouchEnabled:是否开启多点触控


* exclusiveTouch :多个控件接受事件时的排他性

---------------------------------------------------------------

### 2. iOS中事件的产生和传递
1.发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中

2.UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件


事件的具体传递过程,如图:


![150807180781051.png](http:https://img.haomeiwen.com/i2591298/8692cfacbc2c9a54.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/240)

一般事件的传递是从父控件传递到子控件的

例如:

点击了绿色的View,传递过程如下:UIApplication->Window->白色View->绿色View

点击蓝色的View,传递过程如下:UIApplication->Window->白色View->橙色View->蓝色View

如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件


### ( 重难点)如何寻找最合适的view

应用如何找到最合适的控件来处理事件?有以下准则
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
4.如果没有符合条件的子控件,那么就认为自己最合适处理

详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。

注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。



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


* 不接受用户交互:userInteractionEnabled = NO;

* 隐藏:hidden = YES;

* 透明:alpha = 0.0~0.01



寻找最合适的view过程,如图:

![150807180781052.png](http:https://img.haomeiwen.com/i2591298/106eb30915d918e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/240)

说明一下控件的添加顺序:白1->绿2->橙2->蓝3->红3->黄4

这里点击了橙色的那块区域,事件传递判断过程如下:

1.UIApplication从事件队列中取出事件分发给UIWindow

2.UIWindow判断自己是否能接受触摸事件,可以

3.UIWindow判断触摸点是否在自己身上,是的。

4.UIWindow从后往前便利自己的子控件,取出白1

5.白1都满足最上面两个条件,遍历子控件橙2

6.橙2都满足最上面两个条件,遍历子控件,先取出红3

7.红3不满足条件2,取出蓝3

8.蓝3也不满足条件2,最后最合适的控件是橙2


#### 在事件传递寻找最合适的View时,底层到底干了哪些事?

寻找合适的View用到两个重要方法:

*  hitTest:withEvent:
 
*  pointInside

####  hitTest:withEvent:方法
什么时候调用?

* 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View



作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,
事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法


hitTest:withEvent:底层调用流程:

![123.png](http:https://img.haomeiwen.com/i2591298/d188efbf373a2a4d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/540)

底层具体实现如下 :

  • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
    // 1.判断当前控件能否接收事件
    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;
    }

**事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。**



 hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。
      如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。

** - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event **
该方法判断触摸点是否在控件身上, 是则返回YES, 否则返回NO

**作用
   可以使用以上两个方法做到:
   指鹿为马(明明点击的是B视图, 却由A视图来响应事件)
   穿透某控件点击被覆盖的下一层控件
   让父控件frame之外的子控件响应触摸事件(下面实际应用中有具体介绍)**
   
   

---------------------------------------------------------------


### 3.事件响应
上文介绍了事件的传递过程,找到合适的View之后就会调用该view的touches方法要进行响应处理具体的事件,找不到最合适的view,就不会调用touches方法进行事件处理。

这里先介绍一下响应者链条:响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条

一般默认做法是控件将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理 (即调用super的touches方法)。

那么如何判断当前响应者的上一个响应者是谁呢?有以下两个规则:

1.判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器

2.如果不是控制器的View,上一个响应者就是父控件

响应过程如下图:

![150807180781053.png](http:https://img.haomeiwen.com/i2591298/5c25578f2e8d950b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


touch响应:

* 找到最合适的view会调用touches方法处理事件
* touches默认做法是把事件顺着响应者链条向上抛

//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件

  • (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
    [super touchesBegan:touches withEvent:event];
    // 注意不是调用父控件的touches方法,而是调用父类的touches方法
    // super是父类 superview是父控件
    }




如果控制器也不响应响应touches方法,就交给UIWindow。如果UIWindow也不响应,交给UIApplication,如果都不响应事件就作废了。

用具体的例子来看下响应过程,例子太简单就不上demo了,看下截图(3-2)

![c50b227db9693bf974cf4f9e01eb9a98.jpg](http:https://img.haomeiwen.com/i2591298/7b0ba27676160e08.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/340)
(图3-2)

   控件的添加顺序:红1->蓝2->绿2->黄3

![屏幕快照 2016-08-28 下午1.56.37.png](http:https://img.haomeiwen.com/i2591298/041859d799cf9101.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/740)

绿色View实现了touch方法,如下
![屏幕快照 2016-08-28 下午1.58.41.png](http:https://img.haomeiwen.com/i2591298/d4fbf11a7f05de63.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/740)

黄色view没有实现touch方法,如下


![屏幕快照 2016-08-28 下午1.58.52.png](http:https://img.haomeiwen.com/i2591298/6b8902d4a995f9b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/740)

当点击黄色区域时,由于黄色view没有实现touch方法,就顺着响应链找到其父view(绿色View),绿色view实现了touch方法,便打印了-- touchGreen

最后总结来说一次完整的触摸事件的传递响应过程为:

UIApplication-->UIWindow-->递归找到最合适处理的控件-->控件调用touches方法-->判断是否实现touches方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者-->找不到方法作废

一句话总结整个过程是:触摸或者点击一个控件,然后这个事件会从上向下(从父->子)找最合适的view处理,找到这个view之后看他能不能处理,能就处理,不能就按照事件响应链向上(从子->父)传递给父控件

事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。


### 4 实际项目中的应用

* 情景1: 点击子控件,让父控件响应事件;(点击绿色View,红色View响应)

分析:可通过两种方式实现

(1)因为hitTest:withEvent:方法的作用就是控件接收到事件后,判断自己是否能处理事件,判断点在不在自己的坐标系上,然后返回最合适的view。所以,我们可以在hitTest:withEvent:方法里面强制返回父控件为最合适的view.



import "GreenView2.h"

@implementation GreenView2

  • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
    return [self superview]; // return nil;
    // 此处返回nil也可以。返回nil就相当于当前的view不是最合适的view
    }

@end



 (2) 让谁响应,就直接重写谁的touchesBegan: withEvent:方法
 

import "RedView1.h"

@implementation RedView1

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{

NSLog(@"-- touchRed touchesBegan");

}

@end


* 情景2:点击子控件,父控件和子控件都响应事件

分析:事件的响应是顺着响应者链条向上传递的,即从子控件传递给父控件,touch方法默认不处理事件,而是把事件顺着响应者链条传递给上一个响应者。这样我们就可以依托这个原理,让一个事件多个控件响应

import "GreenView2.h"

@implementation GreenView2

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"-- touchGreen");
[super touchesBegan:touches withEvent:event];
}


参考资料:

[史上最详细的iOS之事件的传递和响应机制](http://www.jianshu.com/p/2e074db792ba)

[iOS中的事件传递和响应者链条](http://www.linuxidc.com/Linux/2015-08/121270.htm)

[自实现UIKit框架](https://github.com/BigZaphod/Chameleon)

[iOS触摸事件处理详解](http://www.jianshu.com/p/cb0314b72883?nomobile=yes)

相关文章

  • 网络收藏

    ios点击事件传递及响应http://blog.flight.dev.qunar.com/2016/10/28/i...

  • iOS 点击事件传递及响应

    参考文档:http://blog.flight.dev.qunar.com/2016/10/28/ios-even...

  • iOS 点击事件传递及响应

    前言:苹果的官方文档《Event Handling Guide for iOS》对事件处理做了非常详尽清晰的解释,...

  • iOS 点击事件传递及响应

    1.iOS中的事件 iOS中的事件可以分为3大类型: 触摸事件加速计事件远程控制事件这里我们只讨论iOS中的触摸事...

  • iOS 点击事件传递及响应

    iOS 点击事件传递及响应 原文作者:苦工原文链接:https://www.jianshu.com/p/f55b6...

  • iOS 点击事件传递及响应

    一、前言 首先附上苹果的官方文档《Event Handling Guide for iOS》 二、目录 1.iOS...

  • iOS点击事件传递及响应

    依据苹果的官方文档《Event Handling Guide for iOS》对事件处理进行了整理. 响应者对象(...

  • iOS事件与响应

    参考文件:1.官方文档2.iOS触摸事件那点儿事3.iOS 点击事件传递及响应4.iOS事件处理,看我就够了 1....

  • iOS 响应链

    iOS开发 - 事件传递响应链iOS 响应者链,事件的传递事件传递之响应链Cocoa Touch事件处理流程--响...

  • 深入浅出iOS事件机制

    深入浅出iOS事件机制事件传递:响应链事件传递响应链

网友评论

  • 半熟木屋:[self.superView touchesBegan:touches withEvent:event];
  • 沧海小鱼儿:浪的很
  • df7fa75b0524:博主你好,请教一个问题,我有个页面布局是这样的,底层scrollView,表层头部放一个headview,剩余部分是tableview。碰到的问题是当手指放到tableView上进行向上滚的时候,tableView和那个header一起滚上去了。我想让tableView不滚。当header完全滚没了后,在让tableview滚动,这里涉及到scrollview滚动和tableview滚动方法重叠问题,请问应该怎么处理呢
  • 03d0aef2a797:为什么手势和单击事件只会响应手势?一种猜测: ---- 原因是:
    UIGestureRecognizer 有个属性cancelsTouchesInView,这个属性默认值是YES,即当手势识别成功后,会发送touchesCancelled消息给view来结束view的响应。
    如果cancelsTouchesInView为NO,那么gestureRecognizer和view都可以响应
    苦工:涨姿势了 :+1:
  • 流浪的小乌龟:加速计事件 应该改为 传感器事件 更为合适,因为检测手机转动是用的陀螺仪,指南针是利用的磁力计,这些都归于传感器之列。
    流浪的小乌龟:@旧饭盆mingzhi_liu 赞一个:+1:
    苦工:@流浪的小乌龟 OK
  • 653e225ef689:一堆人只注意到了事件传递,和事件响应,,,,,难道没注意到事件响应必须是完整的触摸吗? 也就是说要想让多个响应者同时响应,就必须从touchBegan开始分发响应事件,,只从其他的触摸事件(moved,cancelled)开始是没有任何效果的.....
    653e225ef689:@旧饭盆mingzhi_liu 我也是在代码调试的过程发现的,很多文章竟没有提到这个,真不敢相信
    苦工:@653e225ef689 是的。事件必须有一个起始点
  • 653e225ef689:最新的好像只支持touchBegan,如果下一个响应者没有实现touchBegan和super touchBegan,就不会传递给上一个响应者
  • MrZombie:可以,最喜欢这种讲原理的文章了
  • FicowShen:谢谢博主!写得蛮细致的了,刚刚阿里远程面试问到了这个问题,然后。。。。:joy: :joy:
    FicowShen:@旧饭盆mingzhi_liu 有帮助,有帮助:smile: :smile:
    苦工:客气了。希望对你有帮助
  • hhhhxy:可否说一下hitTest的顺序
    苦工:@iOS_hxy 最近比较忙,刚刚有时间把hitTest补充了一下,有问题欢迎指正
  • 霜白露:写的很详细 赞一个
    苦工:谢谢:blush:
  • f6460dd70ca8:不错 也是经典面试题
    苦工:@fancy888 哈哈。客气 一起学习
    f6460dd70ca8:@旧饭盆mingzhi_liu 以后开发上的技术难题还请多多指教,我刚转行iOS开发:smiley:
    苦工:@fancy888 :smile:

本文标题:iOS 点击事件传递及响应

本文链接:https://www.haomeiwen.com/subject/ngszsttx.html