iOS开发中的事件处理

iOS开发中的事件处理

理论非原创,是对网上资料的整理以及Demo验证

一. UIResponder

1.1 事件处理简介

iOS中的事件:
在用户使用app的过程中会产生各种各样的事件

0-2.png

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”;
我们熟悉的 UIApplication、 UIViewController 和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,都能够接收并处理事件。需要注意的是Core Animation layers 不是responders

第一响应者是指定的第一个接收事件的对象,比如一个View对象是第一响应者.一个对象可以通过以下方法成为第一响应者:

  1. 重写canBecomeFirstResponder 方法,返回yes
  2. 接收到一个 becomeFirstResponder的menssage,如果需要,也可以自己给自己发送message成为第一响应者.

注意:确保你的app指定一个对象成为第一响应者之前,这个对象图像已经被绘画出来.
举个例子:通常你在重写 viewDidAppear方法内,执行becomeFirstResponder方法.但是如果在viewWillAppear:中 指定 第一响应者,你的对象的图像还没有被建立好,所以becomeFirstResponder会返回NO

1.2 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的触摸事件处理
UIView是UIResponder的子类,可以实现下列4个方法处理不同的触摸事件

一根或者多根手指开始触摸view,系统会自动调用view的下面方法

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

一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)

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

一根或者多根手指离开view,系统会自动调用view的下面方法

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

触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法

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

提示:touches中存放的都是UITouch对象

1.3 UITouch:

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象

UITouch的作用:

保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象

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;

UITouch的方法:
- (CGPoint)locationInView:(UIView *)view; 

返回值表示触摸在view上的位置 
这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0)) 
调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view; 
该方法记录了前一个触摸点的位置

UITouch的phase有什么用?
触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。而通过phase可以查看当前触摸事件在一个周期中所处的状态。phase是UITouchPhase类型的,这是一个枚举配型,包含了

 UITouchPhaseBegan(触摸开始)
 UITouchPhaseMoved(接触点移动)
 UITouchPhaseStationary(接触点无移动)
 UITouchPhaseEnded(触摸结束)
 UITouchPhaseCancelled(触摸取消)

1.4 UIEvent

UIEvent:每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型
常见属性

事件类型

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

@property(nonatomic,readonly) NSTimeInterval timestamp;

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

Touches和event参数:
一次完整的触摸过程,会经历3个状态:

触摸开始:- (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

4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数

一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数

如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸

疑问:默认触摸方法NSSet里面只能获得一个UITouch对象,为什么?

UIView默认不支持多点触控。也就是说不支持多只手指同时触摸。

如何让视图接收多点触摸?

需要设置它的multipleTouchEnabled属性为YES,默认状态下这个属性值为NO,即视图默认不接收多点触摸。。

如何判断用户当前是双击还是单击?

根据UITouch的tapCount属性的值。tapCount表示短时间内轻击屏幕的次数。因此可以根据tapCount判断单击、双击或更多的轻击。

根据tapCount点击的次数来设置当前视图的背景色(双击改变背景颜色)
轻击操作很容易引起歧义,比如当用户点了一次之后,并不知道用户是想单击还是只是双击的一部分,或者点了两次之后并不知道用户是想双击还是继续点击。为了解决这个问题,一般可以使用“延迟调用”函数。

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    if(touch.tapCount != 2){ // 如果不是双击
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setBackgroundColor:)  object:[UIColor orangeColor]];
    } else { // 延时1执行改变背景的方法
        [self performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0];
    }
}

提示:iPhone开发中,要避免使用双击事件!

NSObject类的cancelPreviousPerformRequestWithTarget:selector:object方法取消指定对象的方法调用。
官方对该方法解释:
Cancels perform requests previously registered with performSelector:withObject:afterDelay:.
All perform requests are canceled that have the same target as aTarget, argument as anArgument, and selector as aSelector.
如果是带参数,那取消时的参数也要一致,否则不能取消成功

细节

检测tapCount可以放在touchesBegan也可以touchesEnded,不过一般后者更准确,因为touchesEnded可以保证所有的手指都已经离开屏幕,这样就不会把轻击动作和按下拖动等动作混淆。

不管是一个手指还是多个手指,轻击操作都会使每个触摸对象的tapCount加1,因此可以直接调用touches的anyObject方法来获取任意一个触摸对象然后判断其tapCount的值即可。

二. 事件传递,响应者链条

简介:

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件 队列中,UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)

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

不接收用户交互
userInteractionEnabled = NO

隐藏
hidden = YES

透明
alpha = 0.0 ~ 0.01

提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

2.1 事件传递的详细过程:

概念: iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。

UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。

**hitTest:withEvent:方法的处理流程如下: **

  • 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
  • 若返回NO,则hitTest:withEvent:返回nil;
  • 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图(top)一直到到最底层视图(bottom),即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
  • 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
  • 如所有子视图都返回NO,则hitTest:withEvent:方法返回自身(self)。


    1-1-0-1.png

假如用户点击蓝色3view
下面结合上图介绍hit-test view的流程:

  1. 白1是UIWindow的根视图,因此,UIWindwo对象会首相对白1进行hit-test;

  2. 显然用户点击的范围是在白1的范围内,因此, pointInside:withEvent:返回了YES,这时会继续检查白1的子视图;

  3. 这时候会有两个分支,绿色2红色2

    点击的范围不再 绿2 内,因此 绿2 分支的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

    点击的范围在 红2 内,即 红2 的 pointInside:withEvent:返回YES;

  4. 这时候有 蓝3绿3 两个分支:

    点击的范围不再 绿3 内,因此 绿3 的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

    点击的范围在 蓝3 内,即 蓝3 的 pointInside:withEvent:返回YES,而由于再对 蓝3 的子视图进行hit-test时全部返回了nil(也可以理解由于蓝3没有子视图),因此,蓝3的 hitTest:withEvent:会将 蓝3 返回,再往回回溯,就是 红2 的 hitTest:withEvent:返回 蓝3 --->> 白1 的hitTest:withEvent:返回 蓝3

  5. 蓝3是包含touch的所有view中层级最小的,所以他就是hit-test view

  6. 至此,本次点击事件的第一响应者(蓝3)就通过响应者链的事件分发逻辑成功的找到了。

这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。

本次事件的传递顺序: UIApplication --> UIWindow -->白色BASEView1 -->红色2 -->蓝色3
上面找到了事件的第一响应者,接下来就该沿着寻找第一响应者的相反顺序来处理这个事件,如果UIWindow单例和UIApplication都无法处理这一事件,则该事件会被丢弃。

说明:
  1. 如果最终 hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;
  2. hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
  3. 触摸事件的传递是从父控件传递到子控件
  4. <font color=#FF0000>如果父控件不能接收触摸事件,那么子控件就不能接收到触摸事件</font>
  5. 如上图情况,如果想在点击黄4的时候,让蓝3成为第一响应者,而让黄4不响应,可以有以下几种方法:
    5.1 设置黄4隐藏(hidden=YES),禁止用户操作(userInteractionEnabled=YES),alpha级别小于0.01(alpha<0.01)
    5.2 重写黄4的pointInside:withEvent:方法, 返回NO
    5.3 重写蓝3的 hitTest:withEvent:方法 如果得到的view是黄4,返回self
示例:

点击�绿色view:
UIApplication --> UIWindow -->白色BASEView -->绿色View


1-1-1-1.png

点击半透明绿色view:


1-1-8-1.png

UIApplication --> UIWindow -->白色BASEView -->红色 -->半透明绿色

点击黄色View:


1-1-5-1.png

UIApplication --> UIWindow -->白色BASEView -->红色 -->蓝色 -->黄色

主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,但是这仅仅是整个事件处理过程的第一步 找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理
touchesBegan…
touchesMoved…
touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理

如何找到最合适的控件来处理事件?
  1. 自己是否能接收触摸事件?-->否-->事件传递到此结束
  2. 触摸点是否在自己身上?-->否-->事件传递到此结束
  3. 从后往前遍历子控件,重复前面两个步骤
  4. 如果没有符合条件的子控件,那么就自己最合适

2.2 定制hitTest:withEvent:方法

如果父视图需要对对哪个子视图可以响应触摸事件做特殊控制,则可以重写hitTest:withEvent:或pointInside:withEvent:方法。
这里有几个例子:


2-2.png

在此例子中button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *result = [super hitTest:point withEvent:event];
    
    CGPoint buttonPoint = [_underButton convertPoint:point fromView:self];
    if ([_underButton pointInside:buttonPoint withEvent:event]) {
        return _underButton;
    }
    
    return result;
}

2.3 响应者链条

响应者:继承了UIResponder的对象就是响应者

响应者链条:

  1. 由多个响应者对象连接起来的链条叫做响应者链条
  2. 什么是上一个响应者?
    2.1. 如果当前这个view是控制器的view,控制器就是上一个响应者
    2.2. 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
  3. 利用响应者链条可以让多个控件处理同一个 "触摸事件"
    3.1. 在最后适合的控件里调用super的touchesBegan方法,这样就将事件传给上一个响应,上一个响应者也可以处理事件了

响应者链条,总的来说是:

  1. 它是一种事件处理机制,由多个响应者对象连接起来的链条,使得事件可以沿着这些对象进行传递。
  2. 如果一个响应者对象不能处理某个事件或动作消息,则将该事件消息重新发送给链中的上一个响应者。
  3. 消息沿着响应者链向上、向更高级别的对象传递,直到最终被处理。

事件的完整处理过程:

  1. 先将事件对象由上往下传递(由父控制传给子控件),找到最适合的控件来处理

  2. 调用最合适的控件的touches...方法

  3. 如果调用了[super touch…],就会将事件顺着响应都链往上传递,传递给上一个响应者

  4. 接着上一个响应者就会调用的touches...方法

  5. 如果没有找到最适合的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给UIApplication

  6. 如果Applicatoin不能处理事件,则将其丢弃

响应链中事件的传递遵循特殊的派送轨道

一个初始对象为hit-test view 或者first responder,如果这个对象不处理事件,UIKit会将这个事件传递给响应链中的next responder,每一个responder决定是自己处理该事件,还是将事件传递给自己的next responder.这个过程一直持续,直到事件被处理或者没有更多的responder了.

当iOS 检查出一个事件并查到事件的第一响应者(具有代表性的是个view),响应者链队列开始

响应者链条示意图:

1-2.png

左边的app,事件遵循以下流程:

  1. initial view 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的superview,因为它不是它的view controller里view层级的最高层.
  2. superview 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的superview,因为它仍旧不是它的view controller里view层级的最高层.
  3. topmost view 是view controller的view层级的最高层,它尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的view controller.
  4. view controller 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给window.
  5. 如果window object 不能处理事件,它直接把事件传递给singleton app object
  6. 如果app object 不能处理事件,它会丢弃这个事件

右边的app遵循略微不同的路线,但是所有的事件event传递遵循下边的启发法:

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

推荐阅读更多精彩内容