iOS中有三类事件:UIEventTypeTouches触摸事件、 UIEventTypeMotion “动作”事件,比如摇晃手机设备、UIEventTypeRemoteControl远程控制事件。还有一种在iOS9.0之后出现的UIEventTypePresses事件,和触按物理按钮有关。
三大类事件分别有一些子事件:

响应者对象:不过在ios中不是任何对象都可以处理事件,只有继承了UIResponder的对象才能接收处理事件,比如UIApplication、UIViewController、UIView、UIWindow。

触摸事件

UIView是UIResponder的子类。UIResponder有以下四个方法处理触摸事件,UIView可以重写这些方法去自定义事件处理。

  1. 一根或者多根手指开始触摸view(手指按下)

  2. -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event

  3. 一根或者多根手指在view上移动(随着手指的移动,会持续调用该方法)

  4. -(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event

  5. 一根或者多根手指离开view(手指抬起)

  6. -(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event

  7. 某个系统事件(例如电话呼入)打断触摸过程

  8. -(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event

对于这四个触摸事件处理方法的参数的说明:

  • 第一个参数:(NSSet)touches*

NSSet和 NSArray类似

但NSSet的区别在:
1.无序不重复(哈希)。与添加顺序也没有关系,也不能通过序号来取出某个元素;即使多次重复添加相同的元素,储存的都只有一个。
2.通过 anyObject方法来随机访问单个元素。
3.如果要访问NSSet 中的每个元素,通过for in循环遍历。
4.好处: 效率高。比如重用 Cell 的时候, 从缓存池中随便获取一个就可以了, 无需按照指定顺序来获取; 当需要把数据存放到一个集合中, 然后判断集合中是否有某个对象的时候

touches参数中存放的都是UITouch对象。

UITouch

当用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。如果两根手指同时触摸屏幕,则会调用一次touchesBegan方法,创建两个UITouch对象(如果不是同时触摸,调用两次方法,每次的touches参数都只有一个UITouch对象)。
判断是否多点触摸:NSSet有多少个UITouch对象元素。

UITouch保存着跟本次手指触摸相关的信息,比如触摸的位置、时间。当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指的触摸位置。当手指离开屏幕时,系统会销毁相应的UITouch对象。
比如,判断单击、双击或者多击:tapCount属性

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

  2. UITouch * touch = touches.anyObject;//获取触摸对象

  3. NSLog(@"%@",@(touch.tapCount));//短时间内的点击次数

  4. }

UITouch常用方法:
-(CGPoint)locationInView:(UIView*)view;返回触摸在参数view上的位置,该位置基于view的坐标系(以view的左上角为原点(0, 0));如果调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
-(CGPoint)previousLocationInView:(UIView*)view;前一个触摸点的位置,参数同上

  • 第二个参数(UIEvent)event*
    每产生一个事件,就会产生一个UIEvent对象,UIEvent保存事件产生的事件和类型。UIEvent还提供了相应的方法可以获得在某个view上面的UITouch触摸对象。
    一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数.

UIView无法与用户交互的情况

  1. userInteractionEnabled= NO 如果父视图不能与用户交互, 那么所有子控件也不能与用户交互
  2. hidden= YES
  3. alpha= 0.0 ~ 0.01
  4. 子视图的位置超出了父视图的有效范围, 那么子视图超出部分无法与用户交互的
  5. UIImageView的userInteractionEnabled默认是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

事件的传递&响应

事件传递中UIWindow会根据不同的事件类型(3种),用不同的方式寻找initial object。比如Touch Event,UIWindow会首先试着把事件传递给事件发生的那个view,就是下文要说的hit-testview。对于Motion和Remote Event,UIWindow会把例如震动或者远程控制的事件传递给当前的firstResponder

寻找响应者Hit-Test&Hit-Test View

寻找响应消息.png

Hit-Test的目的就是找到手指点击到的最外层的那个view。它进行类似于探测的工作,判断是否点击在某个视图上。

Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.

  • 什么时候Hit-Test
    与Hit-Test 相关有两个方法:
  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

  2. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

runloop
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中;UIApplication会从事件队列中取出最前面的事件并将其分发处理,通常,先发送事件给应用程序的主窗口UIWindow。UIWindow会调hitTest:withEvent:方法,(从后往前遍历subviews数组)找到点击的点在哪个subview,然后继续调用subView的hitTest:withEvent:方法,直到在视图继承树中找到一个最合适的子视图来处理触摸事件,该子视图即为hit-test view。

这个view和它上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一。我们可以看到UITouch.h里有一个view和gestureRecognizers的属性,就是Hit-Test view和它的手势。

  • ** hitTest:withEvent:如何找到最合适的控件来处理事件**
    1.判断自己是否能接收触摸事件(能否与用户交互)
    2.触摸点是否在自己身上? 调用pointInside:withEvent:
    3.从后往前遍历子控件数组,重复前面的两个步骤 (从后往前:按照addsubview的顺序,越晚添加的越先访问)
    4.如果没有符合条件的子控件,那么就自己最适合处理
    找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

拦截事件传递,可以使用pointInside:withEvent:方法,在实现里面直接return NO;即可,那么hitTest:withEvent:方法返回nil。又或者在hitTest:withEvent:直接return self;不传递给子视图。

摘自网络:hitTest:方法内部的参考实现

  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

  2. {

  3. NSLog(@"%@----hitTest:", [self class]);

  4. // 如果控件不允许与用户交互那么返回 nil

  5. if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {

  6. return nil;

  7. }

  8. // 如果这个点不在当前控件中那么返回 nil

  9. if (![self pointInside:point withEvent:event]) {

  10. return nil;

  11. }

  12. // 从后向前遍历每一个子控件

  13. for (int i = (int)self.subviews.count - 1; i >= 0; i--) {

  14. // 获取一个子控件

  15. UIView *lastVw = self.subviews[i];

  16. // 把当前触摸点坐标转换为相对于子控件的触摸点坐标

  17. CGPoint subPoint = [self convertPoint:point toView:lastVw];

  18. // 判断是否在子控件中找到了更合适的子控件

  19. UIView *nextVw = [lastVw hitTest:subPoint withEvent:event];

  20. // 如果找到了返回

  21. if (nextVw) {

  22. return nextVw;

  23. }

  24. }

  25. // 如果以上都没有执行 return, 那么返回自己(表示子控件中没有"更合适"的了)

  26. return self;

  27. }

要扩大view的点击区域,比如要扩大按钮的点击区域(按钮四周之外的10pt也可以响应按钮的事件),可以怎么做呢?或许重写hittest:withEvent:是个好办法,hitest就是返回可以响应事件的view,在button的子类里面重写它,判断如果point在button的frame之外的10pt内,就返回button自己。

事件响应

什么是第一响应者?简单的讲,第一响应者是一个UIWindow对象接收到一个事件后,第一个来响应的该事件的对象。
如果hit-test视图不处理收到的事件消息,UIKit则将事件转发到响应者链中的下一个响应者,看其是否能对该消息进行处理。
响应链

所有的视图按照树状层次结构组织,每个view都有自己的superView,包括vc的self.view: 
1.当一个view被添加到superView上的时候,它的nextResponder就会被指向它的superView; 
2.当vc被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller; 
(概括前两者就是:如果当前这个view是控制器的self.view,那么控制器就是上一个响应者 如果当前这个view不是控制器的view,那么父控件就是上一个响应者) 
3.vc的nextResponder会被指向self.view的superView。 
4.最顶级的vc的nextResponder指向UIWindow。 
5.UIWindow的nextResponder指向UIApplication 
这就形成了响应链。并没有一个对象来专门存储这样的一条链,而是通过UIResponder的串连起来的。

对于touches方法的描述:

The default implementation of this method does nothing. However immediate UIKit subclasses of UIResponder, particularly UIView, forward the message up the responder chain. To forward the message to the next responder, send the message to super (the superclass implementation); do not send the message directly to the next responder. For example,
[super touchesBegan:touches withEvent:event];
If you override this method without calling super (a common use pattern), you must also override the other methods for handling touch events, if only as stub (empty) implementations.

touches方法实际上什么事都没做,UIView继承了它进行重写,就是把事件传递给nextResponder,相当于[self.nextResponder touchesBegan:touches withEvent:event]。所以当一个view没有重写touch事件,那么这个事件就会一直传递下去,直到UIApplication。如果重写了touch方法,这个view响应了事件之后,事件就被拦截了,它的nextResponder不会收到这个事件。这个时候如果想事件继续传递下去,可以调用[super touchesBegan:touches withEvent:event],不建议直接调[self.nextResponder touchesBegan:touches withEvent:event]
调用[super touches...](实际运行打断点查看:之后父类响应touches,一直传递下去,最后UIResponse来响应touches,然后再由下一个响应者响应touches;前提是它们都重写了touches方法,以及调用[super touches...]
附上一个响应链传送门

不过UIScrollview的touches响应又是另一回事。

响应链事件传递(向上传递)
1.如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图
2.在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3.如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4.如果UIApplication也不能处理该事件或消息,则将其丢弃

总结:
监听事件的基本流程:
1> 当应用程序启动以后创建 UIApplication 对象
2> 然后启动“消息循环”监听所有的事件
3> 当用户触摸屏幕的时候, "消息循环"监听到这个触摸事件
4> "消息循环" 首先把监听到的触摸事件传递了 UIApplication 对象
5> UIApplication 对象再传递给 UIWindow 对象
6> UIWindow 对象再传递给 UIWindow 的根控制器(rootViewController)
7> 控制器再传递给控制器所管理的 view
8> 控制器所管理的 View 在其内部搜索看本次触摸的点在哪个控件的范围内
9> 找到某个控件以后(调用这个控件的 touchesXxx 方法), 再一次向上返回, 最终返回给"消息循环"
10> "消息循环"知道哪个按钮被点击后, 在搜索这个按钮是否注册了对应的事件, 如果注册了, 那么就调用这个"事件处理"程序。(一般就是执行控制器中的"事件处理"方法)

手势

手势识别和触摸事件是两个独立的事,不要混淆。
通过touches方法监听view触摸事件,有很明显的几个缺点:必须得自定义view、由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件、不容易区分用户的具体手势行为。

iOS3.2之后, 把触摸事件做了封装, 对常用的手势进行了处理, 封装了6种常见的手势
UITapGestureRecognizer(敲击)
UILongPressGestureRecognizer(长按)
UISwipeGestureRecognizer(轻扫)
UIRotationGestureRecognizer(旋转)
UIPinchGestureRecognizer(捏合,用于缩放)
UIPanGestureRecognizer(拖拽)

下面谈几个在项目中遇到的问题:
关于手势和touch的相互影响

tap的cancelsTouchesInView方法

“A Boolean value affecting whether touches are delivered to a view when a gesture is recognized.”也就是说,可以通过设置这个布尔值,来设置手势被识别时触摸事件是否被传送到视图。
当值为YES(默认值)的时候,系统会识别手势,并取消触摸事件;为NO的时候,手势识别之后,系统将触发触摸事件。

  • 把手势添加到btn上
  1. - (void)viewDidLoad {

  2. UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];

  3. button.backgroundColor = [UIColor redColor];

  4. [self.view addSubview:button];

  5. [button addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];

  6. UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];

  7. tap.cancelsTouchesInView = NO;

  8. [button addGestureRecognizer:tap];

  9. }

  10. - (void)tapAction:(UITapGestureRecognizer *)sender {

  11. NSLog(@"tapAction");

  12. }

  13. - (void)btnAction:(UIButton *)btn {

  14. NSLog(@"btnAction");

  15. }

当cancelsTouchesInView为NO的时候,点击按钮,会先后触发“tapAction:”和“btnAction:”方法;而当cancelsTouchesInView为YES的时候,只会触发“tapAction:”方法。

  • 把手势添加到btn的父view上即[self.view addGestureRecognizer:tap];
    cancelsTouchesInView=NO,点击按钮,会先后触发“tapAction:”和“btnAction:”方法;cancelsTouchesInView=YES,只会触发按钮方法不会触发手势。

  • 但如果不是btn而是别的控件,把手势添加到控件的父view上
    项目中用到的是collectionView,cancelsTouchesInView=NO,点击collectionViewCell,先后触发手势和Cell,cancelsTouchesInView=YES只会触发手势。

对于UIButton,UISlider等继承自UIControl的控件,都会先响应触摸事件,从而阻止手势事件。手势可以理解为是“特殊的层”。对于TableView,CollectionView这种弱点击事件,系统优先响应手势,如果要响应Cell点击事件就要实现代理方法

实现手势的代理方法对手势进行拦截。

called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch

判断,手势的触击方法是否在控件区域,如果是,则返回NO,禁用手势。否则返回YES.

  1. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{

  2. // NSLog(@"%d",[touch.view isKindOfClass:[UIButton class]]);

  3. if ([touch.view.superview isKindOfClass:[UICollectionViewCell class]]) {//如果点击的是UICollectionViewCell,touch.view是collectionViewCell的contentView,contentView的父view才是collectionCell

  4. return NO;

  5. }else if ([touch.view isKindOfClass:[UIButton class]]) {

  6. return NO;

  7. }

  8. return YES;

  9. }

其他:
项目上没遇到且目前还没有深入了解,先po链接方便以后查:
丢一个传送门讲Gesture Recognizers与事件分发路径的关系:
http://blog.csdn.net/chun799/article/details/8194893
手势的3个混淆属性 cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded: http://www.mamicode.com/info-detail-868542.html


补充

对于UIControl类型的控件,一个给定的事件,UIControl会调用- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event来将action message转发到UIApplication对象,再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上,如果没有指定target(即nil),则会将事件分发到响应链上第一个想处理消息的对象上。而如果UIControl子类想监控或修改这种行为的话,则可以重写```sendAction: to: forEvent:``。

将外部添加的Target-Action放在控件内部来处理事件,实现如下:

  1. // Btn.m

  2. - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {

  3. // 将事件传递到对象本身来处理

  4. [super sendAction:@selector(handleAction:) to:self forEvent:event];

  5. }

  6. - (void)handleAction:(id)sender {

  7. NSLog(@"handle Action");

  8. }

  9. // ViewController.m

  10. - (void)viewDidLoad {

  11. [super viewDidLoad];

  12. self.view.backgroundColor = [UIColor whiteColor];

  13. Btn *btn = [[Btn alloc]initWithFrame:CGRectMake(30, 30, 100, 100)];

  14. btn.backgroundColor = [UIColor yellowColor];

  15. [btn addTarget:self action:@selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];

  16. [self.view addSubview:btn];

  17. }

  18. - (IBAction)btnclick:(id)sender {

  19. NSLog(@"click");

  20. }

最后处理事件的Selector是Btn的handleAction:方法,而不是ViewController的btnclick:方法。

另外,sendAction:to:forEvent:实际上也被UIControl的另一个方法所调用,即sendActionsForControlEvents:。这个方法的作用是发送与指定类型相关的所有行为消息。我们可以在任意位置(包括控件内部和外部)调用控件的这个方法来发送参数controlEvents指定的消息。在我们的示例中,在ViewController.m中作了如下测试:

  1. - (void)viewDidLoad {

  2. // ...

  3. [btn addTarget:self action:@selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];

  4. [btn sendActionsForControlEvents:UIControlEventTouchUpInside];

  5. }

没有点击btn,触发了UIControlEventTouchUpInside事件,并执行handleAction:方法。

iOS Touches事件处理知识总结相关推荐

  1. iOS Touches事件传递

    直接上图: 上图直观的反映了touches事件的分发顺序和touches事件处理顺序: 一.事件分发顺序,目的找到被触摸的视图 1.iOS判断哪个界面能接受消息是从View层级结构的父View向子Vi ...

  2. iOS开发基础知识--碎片44

    iOS开发基础知识--碎片44  iOS开发基础知识--碎片44 1:App跳转至系统Settings 跳转在IOS8以上跟以下是有区别的,如果是IOS8以上可以如下设置: NSURL *url = ...

  3. iOS开发基础知识--碎片27

     iOS开发基础知识--碎片27 1:iOS中的round/ceil/floorf extern float ceilf(float); extern double ceil(double); ext ...

  4. iOS底层基础知识-文件目录结构

    一:iOS沙盒知识 出于安全考虑,iOS系统把每个应用以及数据都放到一个沙盒(sandbox)里面,应用只能访问自己沙盒目录里面的文件.网络资源等(也有例外,比如系统通讯录.照相机.照片等能在用户授权 ...

  5. iOS开发基础知识--碎片37

    iOS开发基础知识--碎片37 iOS开发基础知识--碎片37 iOS开发基础知识--碎片37 1:iOS 使用NJKWebViewProgress做webview进度条 引入头文件: #import ...

  6. iOS开发基础知识--碎片41

    iOS开发基础知识--碎片41 1:UIWebView加载本地的HTML NSString *path = [[NSBundle mainBundle] bundlePath]; NSURL *bas ...

  7. Swift: iOS底层基础知识-文件目录结构

    一:iOS沙盒知识 出于安全考虑,iOS系统把每个应用以及数据都放到一个沙盒(sandbox)里面,应用只能访问自己沙盒目录里面的文件.网络资源等(也有例外,比如系统通讯录.照相机.照片等能在用户授权 ...

  8. iOS开发基础知识--碎片19

    iOS开发基础知识--碎片19  1:键盘事件顺序 UIKeyboardWillShowNotification // 键盘显示之前 UIKeyboardDidShowNotification // ...

  9. IOS开发基础知识--碎片45

    1:iOS SEL的简单总结 SEL就是对方法的一种包装.包装的SEL类型数据它对应相应的方法地址,找到方法地址就可以调用方法 a.方法的存储位置 在内存中每个类的方法都存储在类对象中 每个方法都有一 ...

最新文章

  1. 领课网络在线教育系统开源项目
  2. gui design studio3 中文帮助(4)-用户界面 (中)-工具面板
  3. Spring-国际化信息01-基础知识
  4. Python常用的模块和简单用法
  5. 办公室自动化系统_RPA:办公自动化的下一站
  6. 真正优秀的人,都过着朴素的生活
  7. 更改Fedora 11的plymouth开机动画
  8. 聚会「AHOI 2008」
  9. PLSQL 升级到最新版本 以及 破解注册细则
  10. 阿里云分布式调度系统-伏羲
  11. java 毕业论文_Java程序设计毕业论文.doc
  12. cd40系列芯片_CD40,CD45系列芯片功能大全
  13. 绘制一只奥特曼DIY
  14. mysql outer join的用法_MySQL 8 中的连接语法JOIN、OUTER JOIN的相关用法
  15. elasticsearch高级搜索功能多维度分享
  16. Python基于人脸识别的考勤系统(附源码)
  17. 大数据营销之用户画像
  18. 学生管理系统【Python】
  19. 如何利用计算机班级成绩分析,北京自考计算机应用基础课成绩分析报告
  20. Mari真实生物纹理绘画制作视频教程

热门文章

  1. Apple A2289 820-01987图纸下载
  2. c语言中求一个3x3的整形矩阵,编程实现求两个3x3矩阵的和(C语言)
  3. Unity3D游戏开发之数据持久化PlayerPrefs的使用
  4. centos8下zookeeper命令行操作
  5. J1-20:《英语辅导系统》关键算法
  6. uni小程序地图map的使用以及地图(去导航)调用本地地图功能
  7. 健康云平台开发说明文档
  8. 为什么我们决定重构 Uber 司机端
  9. 用Linux内核的瑞士军刀-eBPF实现socket转发offload
  10. 虚拟机性能测试:十五 性能分析—终极PK(PCMark综合得分)