iOS 点击事件传递及响应

一、前言

首先附上苹果的官方文档《Event Handling Guide for 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控件外,手势识别器UIGestureRecongnizer类的实例也是处理触屏事件的好帮手,其内部也使用目标行为表。
    为什么手势和单击事件只会响应手势?
    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.主窗口会在视图层次机构中找到一个最合适的视图来处理触摸事件
事件的具体传递过程, 如图:


image.png

一般事件的传递是从父控件传递到子控件的
例如:
点击了绿色的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过程,如图:


    image.png

    说明一下控件的添加顺序:白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:底层调用流程:


image.png
底层具体实现如下 :
- (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,上一个响应者就是父控件
响应过程如下图:


image.png

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,如果都不响应事件就作废了。
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)。

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

推荐阅读更多精彩内容