事件传递:响应链
开发者设计APP时,通常都想要对一些事件进行动态响应。比如,触摸事件可以发生在屏幕上的许多不同的对象上,开发者就需要确定到底想要哪个对象对哪个事件进行响应,并理解该对象是如何接收该事件的。
当用户触摸事件发生时,UIKit框架就会为此创建一个事件对象,该对象自身就包含了能够处理该事件对象所必要的信息。然后UIKit框架将此对象列入active app的事件队列。对于触摸事件,事件对象就是组装的一系列UIEvent对象。对于动作事件,所对应的事件对象因开发者所使用的框架、开发者感兴趣的动作事件类型不同而异。
每一个事件都会沿着特定的路径传递下去,直到某个对象能处理为止。首先,单例对象UIApplication会从事件队列的顶端取出来一个事件并进行派遣,以便开始处理。通常,该事件会被派送给app的主窗体(window)对象,然后由此window对象将事件传递给最初事件发生所在的“初始对象”进行处理,这个初始对象是什么,就依赖于该事件的类型:
- Touch Event(触摸事件)。对于触摸事件,主窗体对象首先尝试将事件传递给事件发生所在的视图view对象。该视图对象view就是所谓的hit-test视图对象。这个寻找hit-test视图对象的过程被称作hit-testing,在章节“Hit-Testing返回触摸事件发生所在的视图对象”有介绍。
- Motion and remote control events(运动和远程控制事件)。对于这些事件,主窗体对象会将摇晃事件或者远程控制事件发送给第一响应器(the first responder)进行处理。第一处理器在章节“响应器链由多个响应器对象组成”。
事件对象路径的最终目标,就是找到能够处理事件并进行响应的对象。因此,UIKit先会将事件发送给最适合处理该事件的对象。对于触摸事件,该对象就是hit-test视图对象,对于其他的事件,该对象就是第一响应器。以下部分会更详细地对hit-test视图对象和所确定的第一响应器进行解释。
1.Hit-Testing返回“发生触摸事件”所在的视图对象
iOS系统使用Hit-Testing去查找到底触摸事件发生在哪个视图对象上。Hit-Testing先检查触摸对象所在的位置是否在对应任意屏幕上的视图对象的区域范围内。如果在的话,就开始对此视图对象的子视图对象进行同样的检查。视图树中最底层那个包含此触摸点位置的视图对象,就是要查找的hit-test视图对象。iOS一旦确定hit-test视图对象,就会把触摸事件传递给它进行处理。
举个例子,假设用户触摸了视图E,如图2-1所示。iOS就会按照以下顺序对子视图进行检查来查找hit-test视图:

- 触摸点在视图A的区域范围内,然后开始检查子视图B和C
- 触摸点不在B的范围而在C的范围,于是就开始检查D和E视图
- 触摸点不在D的范围而在E的范围,而E视图是视图树最底层的并包含触摸点的视图对象,所以E就成为了hit-test视图。
hitTest:withEvent: 方法会返回给定的CGPoint和UIEvent所在的hit-test视图对象。hitTest:withEvent:方法会先调用pointInside: withEvent:方法。如果传入hitTest:withEvent:的CGPoint点对象位于视图对象的区域范围内,pointInside:withEvent:返回值就是YES,然后,该hitTest:withEvent:就会依次在返回YES的子视图对象上调用hitTest:withEvent:。
如果传入hitTest:withEvent:的点不在视图对象的范围内,第一次调用pointInside:withEvent:就会返回NO,这个点就被忽略掉了, hitTest: withEvent: 就返回nil。如果子视图返回NO,那么整个视图树的分支都会被忽略掉,因为如果子视图不包含这个点,那子视图的子视图就更不会包含这些点了。这也就意味着,如果父视图都不包含某个触摸事件的点,子视图即使包含了这个点,也不会接收到此触摸事件,因为只有父视图的区域范围包含了触摸事件发生的位置点,事件才会被继续向子视图传递(即触摸点在 子视图 超出 父视图的frame 部分里)。如果子视图的clipsToBounds属性被设置为NO(亦即子视图超出父视图的部分不会被切割掉,也就是说子视图会有一部分不处在父视图的范围内),这种情况是会发生的。
备注:触摸对象UITouch在其生命周期内会和hit-test视图对象一直关联在一起,即使UITouch在后续的时间里移动并离开该视图对象的范围。
hit-test视图对象拥有最先对触摸事件进行处理的机会,如果hit-test视图对象无法处理该事件,事件对象就会沿着响应器的视图链(参见“响应器链由多个响应器对象组成”)向上传递,直到找到最适合处理该事件的对象为止。
2. 响应器链由多个响应器对象组成
许多类型的事件都依赖于响应器链(responder chain)进行事件传递。响应器链就是一系列的相关联的响应器对象。如果第一个响应器无法处理事件,响应器就会将事件对象传递给响应器链的下一个响应器对象。
一个响应器对象就是一个能够对事件进行处理和响应的实体对象。UIResponder类是所有响应器对象的基类,不仅定义了事件处理的编程接口,同时还定义了通用的响应器行为。★ UIApplication、UIViewController、UIView类的实体对象都是响应器,也就意味着,所有的视图对象和关键控制器对象都是响应器对象。注意Core Animation layers不是响应器。
第一响应器被指定第一个接收事件。通常来讲,第一响应器是一个视图view对象。通过做两件事,一个对象就★ 变成第一响应器:
1.重写canBecomeFirstResponder使其返回YES;
2.接收becomeFirstResponder消息。如果有必要,对象本身可以自己发送此消息。
★ 备注:再将某个对象赋值为第一响应器之前,一定要确保APP已经建立好了对象图谱。比如,通常应该在重写的★ viewDidAppear: 方法中调用becomeFirstResponder方法,但是如果写在了★ viewWillAppear里面,此时因为对象图谱还没有建立起来, becomeFirstResponder 的返回值就NO了。
也不仅仅只是事件对象依赖于响应器链,响应器链可以被用于处理以下所有对象:
- Touch Events(触摸事件)。如果hit-test视图对象无法处理触摸事件,事件就会从hit-test视图沿着响应链网上传递,直到找到合适的处理该事件的对象。
- Motion Events(运动事件)。要使用UIKit处理“摇动”(shake-motion)事件,第一响应器就必须实现方法motionBegan:withEvent:或者motionEnded:withEvent:之一,具体请参见“使用UIEvent检测摇动事件”章节。
- Remote Control Events(远程控制事件)。要对远程控制事件进行处理,第一响应器必须实现基类UIResponder的remoteControlReceivedWithEvent:方法。
- Action messages(动作消息)。当用户操作了某个控件,如按钮button、switch,对应的动作方法的目标是nil,该消息会从以控件视图对象为开始的响应器链被发送出去。
- Editing-menu messages(编辑菜单消息)。当用户点击了编辑菜单的指令,iOS系统就会使用响应器链去查找到对应实现了必要处理方法(如cut:,copy:以及paste:)的对象。要获得更多信息,请参阅章节“显示和管理编辑菜单”和样例代码项目CopyPasteTile。
- Text Editing(文本编辑)。当用户点击某个文本区域(UITextField)或者文本视图(UITextView)时,对应的视图就会成为第一响应器。默认情况下,虚拟键盘会弹出来,而且对应的UITextField或者UITextView就会被选中并变成正在编辑状态。如果开发者觉得合适的话,可以使用自定义的视图来代替默认的软键盘作为用户的输入区域视图。要获取更多信息,请参见章节“自定义数据输入视图”。
当用户点击某个 UITextField或者UITextView的时候,UIKit会自动把将对应的对象设置为第一响应器。对于其他的第一响应器,App必须使用becomeFirstResponder方法显示地进行设置。
3.响应器链的特定传递路径
如果(事件发生所在的)初始对象(要么是hit-test视图,要么是第一响应器)无法对事件进行处理,UIKit就会把事件传递给响应器链的下一个响应器对象。每个响应器对象都可以决定是自己进行事件处理,还是将事件通过方法nextResponder的调用,传递给下一个事件响应器。此过程一直进行下去,直到找到了处理该事件的对象,或者到达了响应器链的最后一个响应器了。
响应器链开始于iOS检测到事件并将其传递到(事件发生所在的)初始对象,通常来讲这个对象是一个视图对象view。初始视图对象会最先有机会对事件进行处理。如图2-2所示,就是两个不同的app中事件的不同的两条事件传递路径。App的事件传递路径由其特定的结构所决定,但所有的事件传递路径都遵循同样的逻辑方法。
★ 先由Hit-Testing “从上到下” 找出被触摸的初始视图对象,然后由初始视图对象“ 从下到上” 传递事件处理,是处理。
★ 左边APP的事件传递路径如下:
- 初始视图对象尝试对事件进行处理,如果无法处理,就会将事件传递给其父视图对象,因为视图树中,初始视图对象也并不是最顶端的对象。
- 父视图也进行同样的尝试,因为同样的原因也只能将事件继续向上传递。
- 视图控制器中最顶层的视图也进行同样的尝试,结果发现也处理不了,于是就传递了视图控制器。
- 视图控制器也一样无法处理,于是继续向上传递给了主窗体对象(window)。
- 主窗体也无法处理,于是就继续传递给app的单例实体对象。
- 如果最后单例实体对象还无法处理,此事件就被丢弃了。

★ 虽然右边的APP传递路径略微不一样,但是事件传递遵循的逻辑方法还是一样的:
- 视图将事件沿着其视图控制器的视图树向上传递,直到最顶端的视图。
- 顶端是图无法处理,就直接传递给视图控制器。
- 视图控制器无法处理,就会将事件传递给其顶端视图所在的父视图。重复1-3,直到到达最顶端的根视图控制器(root view controller)。
- 跟视图控制器将事件传递给主窗体对象。
- 主窗体对象传递给app的单例实体对象。
重点注意:如果开发者自己实现某个视图对象,来处理远程控制(remote control)事件、动作消息(action message)、使用UIKit的摇动(shake-motion)事件,或者编辑菜单消息,千万不要直接将事件或者消息发送给nextResponder,来达到将事件沿着响应器链向上传递的目的,而是实现父类的当前事件处理方法,让UIKit框架来完成响应器链的事件消息传递。
多点触摸事件
通常,开发者可以使用UIKit提供的标准控件和手势识别器来处理大部分的触摸事件。手势识别器允许开发者将触摸事件的识别和对应的动作处理区分开来。在某些情况中,有可能开发者想要根据触摸事件做一些其他事情,比如触摸位置进行绘画,要实现这样的效果,重新实现一次这样的触摸识别处理,是没有什么好处的。如果视图的内容和触摸事件本身紧密相关,开发者可以直接对事件进行处理。当用户“触摸”视图对象时,对触摸事件进行接收,基于事件的属性对时间进行识别处理,并作出合适的响应。
1.创建UIReponder子类
要实现自定义触摸事件处理,首先就要创建一个UIResponder子类。该子类对象可以继承自以下的任一对象类:
| 子类对象 | 选择该子类作为第一响应器的原因 |
| UIView | 用于实现自定义绘制视图 |
| UIViewController | 想要处理其它类型的事件,比如“摇动”(shake-motion)事件 |
| UIControl | 想要实现自定义触摸行为的控件 |
| UIApplication或者UIWindow | 继承实现这两个类,这就很少见了,因为一般人是不会这么干的。 |
然后,针对继承后的子类接收多点触摸事件:
- 子类必须实现UIResponder的触摸事件处理的方法,请参阅章节“在子类中实现触摸事件处理方法”。
- 视图对象要接受触摸事件,必须将其userInteractionEnabled属性值设置为YES。如果子类继承自UIViewController,其管理的视图对象就必须支持用户交互。
接收事件的视图对象必须是可见的,不能是纯透明或隐藏的。
2.在子类中实现触摸事件处理方法
iOS将触摸对象识别为多点触摸序列的一部分。在一次多点触摸序列中,app会将这一系列的事件消息发送给目标响应器。要接收并处理这些消息,响应器对象类就必须实现以下几个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;
备注:这些方法和自定义创建一个手势识别器时所需要实现的方法,拥有同样的签名,在章节“创建自定义手势识别器”中有介绍。
每一个触摸方法都相应的对应触摸事件的一个状态:开始(Began),移动(Moved),结束(Ended)和取消(Canceled)。每个方法都有两个参数:触摸事件集合序列和对应的事件。
触摸事件集合序列就是使用NSSet容器承载的UITouch对象,用于代表当前阶段对应的 新的 或 发生变化的 触摸对象。举个例子,当某个“触摸”对象从Began状态转向Moved状态,app就会调用touchesMoved: withEvent: 方法, 被传入该方法的触摸对象序列集合,就会包含此触摸对象和其他同样处于移动状态的触摸对象。另一个参数是包含了事件发生对应的 所有 触摸对象的UIEvent对象。传入的触摸对象序列集合会有所不一样,因为上一个事件消息之后,可能有一些触摸对象的状态并没有发生变化(也就是没有移动过)。
所有处理触摸事件的视图对象都期望能接收到完整的触摸事件流,所以开发者创建自己的子类时要注意以下几条规则:
- 如果自定义的响应器继承自UIView或者UIViewController,就应该实现所有的事件处理方法。
- 如果子类继承自其他响应器类,其中某些不需要处理的事件方法的实现可以是空的。
- 在所有的方法中,记得调用父类对应的方法实现。
如果开发者尝试不让某个响应器对象,在指定的某个阶段接收触摸事件对象,其导致的结果可能会对应产生未知且非预期的行为。
3.追踪触摸事件的状态和位置
iOS会对多点触摸序列事件进行追踪。iOS会记录每一个触摸对象的属性,包括其状态、位置坐标,移动之前的位置坐标,以及时间戳。使用这些属性来确定如何响应触摸事件。
触摸对象使用phase属性存储其状态值,每一个不同的状态对应一个触摸事件方法。触摸对象存储位置信息的方法有三个:触摸事件所在的窗体对象中的位置,对应所在窗体中视图对象的位置,或者触摸事件所在视图对象中的位置。如图3-1的例子,展示了正在进行的触摸事件对应的触摸对象的位置信息。

当手指触摸屏幕时,对应的触摸对象会在其事件的生命周期内,始终和所处的窗体对象以及视图对象关联,即使之后“触摸”位置不在视图区域范围内。通过触摸对象所在的位置信息,决定需要对触摸事件如何响应。比如,如果两下触摸快而连续,如果这两下发生在同一个视图内,就可以将其当作是一次“双击”事件。一个触摸对象可以同时存储当前位置和前一个位置信息(如果有前一个位置的话)。
4.检索查询触摸对象
在某个事件处理方法中,开发者可从以下两个对象中获取到和事件相关的触摸对象的信息:
- UITouch对象序列集合。被传入的NSSet包含了所有该方法所代表的状态下新生成的、或者发生变化的触摸对象,比如UITouchPhaseBegan对应的touchesBegan:withEvent:方法。
- 事件对象(UIEvent)。被传入的UIEvent对象包含了给定的多点触摸序列事件相关的所有触摸对象。
默认情况下multipleTouchEnabled属性值为NO,这也就意味着一个视图对象仅仅只接收多点触摸序列事件的第一个触摸对象。当该属性为“禁用”(disabled)状态时,开发者通过anyObject方法就只能索引到一个触摸对象,因为整个触摸事件集合对象里就只包含了一个触摸对象。
开发者通过视图对象的locationInView:方法,可以获得触摸事件对象UITouch的位置信息 。传入self,开发者所获得的也就是UITouch所在视图对象坐标系中的位置坐标信息。类似的,属性previousLocationInView:方法就可以获取到触摸点的前一个位置点坐标。开发者还能指定一个触摸事件Tap(点击)几次(tapCount),触摸对象什么时候生成、最后是什么时候发生变化的(时间戳timestamp),以及触摸对象所在的状态(touch phase)。

如果开发者只对特定窗体相关联的触摸对象感兴趣,那就调用UIEvent对象的方法touchesForWindow: 。视图3-3显示了窗体A中的所有触摸对象。

如果开发者感兴趣的是和视图对象关联的触摸对象,那就调用UIEvent对象的方法touchesForView:。如图3-4显示视图对象A的所有触摸对象。

5.处理Tap手势
除了让APP能够识别处理tap手势之外,开发者可能还想将单击、双击和三击给区分开来,此时使用触摸事件的属性tapCount来确定触摸事件对应用户在视图对象中的点击次数。
获取该值的最好的地方就是touchesEnded:withEvent:方法,因为刚好这个方法就对应了用户手指结束点击、离开屏幕的时刻。在多点触摸事件结束时,通过查找触摸事件阶段的点击次数,开发者可以确定手指是不是真的在点击,比如,手指“按下”并“拖动”。代码清单1-3就是一个例子,展示了判断某视图对象中所发生的点击事件是不是“双击”。
Listing 3-1 Detecting a double tap gesture
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *aTouch in touches) {
if (aTouch.tapCount >= 2) {
// The view responds to the tap
[self respondToDoubleTapGesture:aTouch];
}
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}
6.处理Swipe和Drag手势
水平和垂直方向的滑动手势是开发者可以追踪的简单手势之一。要检测滑动手势,就要追踪对应的轴向移动信息。然后,通过检查以下几个问题,来确定触摸移动是不是滑动手势操作:
- 用户的手指移动距离够远吗?
- 手指的移动相对比较直的直线运动吗?
- 移动速度够快、快到可以称之为“滑动”吗?
要回答这些问题,就需要存储触摸对象的初始位置信息,当其移动时再与之进行比较。
代码清单3-2,展示了开发者可以用来检测视图对象中水平方向滑动(swipe)手势的基本方法。在这个例子中,视图对象view有一个startTouchPosition属性用来存储触摸操作的初始位置坐标信息。在toucesEnded:方法中,比较“触摸”结束位置的坐标和初始的位置坐标,来确定这是不是一个“滑动”手势。如果移动得太远或者移动地得不够远,就不认为这是一个滑动手势。代码清单中并没有显示对应方法myProcessRightSwipe:和方法myProcessLeftSwipe:的实现,但是自定义视图对象可以在那里对滑动手势进行处理。
Listing 3-2 Tracking a swipe gesture in a view
#define HORIZ_SWIPE_DRAG_MIN 12
#define VERT_SWIPE_DRAG_MAX 4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
// startTouchPosition is a property
self.startTouchPosition = [aTouch locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint currentTouchPosition = [aTouch locationInView:self];
// Check if direction of touch is horizontal and long enough
if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN
&& fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX){
// If touch appears to be a swipe
if (self.startTouchPosition.x < currentTouchPosition.x) {
[self myProcessRightSwipe:touches withEvent:event];
} else {
[self myProcessLeftSwipe:touches withEvent:event];
}
self.startTouchPosition = CGPointZero;
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
self.startTouchPosition = CGPointZero;
}
注意这段代码并没有检查手势操作事件移动过程的中间位置坐标,这也就意味着该手势可能是在整个屏幕中转了一圈,如果开始点和结束点在一条线上,还是会被认做为滑动手势操作。一个更为精密的滑动手势操作识别器,就需要在touchesMoved:withEvent:方法中检查移动过程中位置信息。要检查垂直方向的滑动手势,开发者可以使用类似的代码,只需要把x和y坐标进行交换一下就可以了。
代码清单3-3,显示了更简单的追踪单指触摸事件的实现,这次是用户拖动视图对象在屏幕中移动。此处,自定义视图对象类仅实现了touchesMoved:withEvent:方法。该方法计算触摸对象在视图中当前位置和前一个位置的差值,然后使用这个差值,来设置试图对象的位置。
Listing 3-3 Dragging a view using a single touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint loc = [aTouch locationInView:self];
CGPoint prevloc = [aTouch previousLocationInView:self];
CGRect myFrame = self.frame;
float deltaX = loc.x - prevloc.x;
float deltaY = loc.y - prevloc.y;
myFrame.origin.x += deltaX;
myFrame.origin.y += deltaY;
[self setFrame:myFrame];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}
7.处理更为复杂的多点触摸序列事件
点击Taps、拖动drags、滑动swipes只使用了一根手指而且易于追踪处理。处理包含有双指、多指的触摸事件就更具有挑战性。开发者可能需要追踪所有状态下的每个触摸事件对象,记录触摸对象发生变化的属性值,并对其内部状态进行适当的修改。要追踪和处理多点触摸,就需要:
- 将视图对象的multipleTouchEnabled属性设置为YES。
- 使用Core Foundation字典对象(CFDictionaryRef)来追踪触摸对象在手势操作事件过程中不同状态下的变化。
在处理多点触摸事件时,开发者通常需要存储某个触摸对象的状态信息,后续用来对触摸对象进行比较。比如,开发者可能会想要比较每个触摸对象的初始位置和终止位置。在方法touchesBegan:withEvent:中,开发者可以通过locationInTheView:方法获取到每个触摸对象的初始位置信息,然后将触摸事件对象的地址作为key值,把这些位置信息存储到某个CFDictionaryRef对象中。然后,在touchesEnded:withEvent:方法中,就可以用来索引到被传入的触摸对象的初始位置,并和其当前位置进行比较。
备注:之所以使用CFDictionaryRef数据类型而不是NSDictionary来追踪触摸点,是因为NSDictionary需要复制其存储的key信息,(以UITouch对象作为key的条件下)而UITouch类却没有遵循 “对象复制所必须的” NSCopying协议。
代码清单3-4就展示了如何在某个Core Foundation的字典中存储UITouch对象初始位置信息。方法cacheBeginPointForTouches方法存储了每个触摸点相对于父视图坐标系的位置信息,这样一来就可以以同样的坐标系为参照,来比较所有触摸点的位置坐标了。
Listing 3-4 Storing the beginning locations of multiple touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self cacheBeginPointForTouches:touches];
}
- (void)cacheBeginPointForTouches:(NSSet *)touches {
if ([touches count] > 0) {
for (UITouch *touch in touches) {
CGPoint *point = (CGPoint*) CFDictionaryGetValue(touchBeginPoints,touch);
if (point == NULL) {
point = (CGPoint *)malloc(sizeof(CGPoint));
CFDictionarySetValue(touchBeginPoints, touch, point);
}
*point = [touch locationInView:view.superview];
}
}
}
代码清单3-5是基于上一个例子写的。这段代码展示了如何从字典对象中索引出初始的位置信息,然后,再获取同样的触摸点对应的当前位置信息,使用这些信息,就可以计算出来几何变形矩阵(这一部分没有显示)。
Listing 3-5 Retrieving the initial locations of touch objects
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
CGAffineTransform newTransform = [self incrementalTransformWithTouches:touches];
}
- (CGAffineTransform)incrementalTransformWithTouches:(NSSet *)touches {
NSArray *sortedTouches = [[touches allObjects]sortedArrayUsingSelector: @selector(compareAddress:)];
// Other code here
CGAffineTransform transform = CGAffineTransformIdentity;
UITouch *touch1 = [sortedTouches objectAtIndex:0];
UITouch *touch2 = [sortedTouches objectAtIndex:1];
CGPoint beginPoint1 = *(CGPoint*)CFDictionaryGetValue(touchBeginPoints,touch1);
CGPoint currentPoint1 = [touch1 locationInView:view.superview];
CGPoint beginPoint2 = *(CGPoint*)CFDictionaryGetValue(touchBeginPoints,touch2);
CGPoint currentPoint2 = [touch2 locationInView:view.superview];
// Compute the affine transform
return transform;
}
下一个例子,如代码清单3-6,并没有使用字典对象追踪触摸点的变化,但是,依然可以处理触摸事件的多点触摸。这个例子展示的是,当手指按压着标记有Welcome字样的视图板,在屏幕内移动时,将视图版也跟着移动。当用户双击视图版时,还会改变其对应的语言。这段代码源自于《MoveMe》的样例工程,有需要的可以阅读一下,从而更好的理解事件处理的整个上下文。
Listing 3-6 Handling a complex multitouch sequence
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// App supports only single touches, so anyObject retrieves just
// that touch from touches
UITouch *touch = [touches anyObject];
// Move the placard view only if the touch was in the placard view
if ([touch view] != placardView) {
// In case of a double tap outside the placard view, update
// the placard's display string
if ([touch tapCount] == 2) {
[placardView setupNextDisplayString];
}
return;
}
// Animate the first touch
CGPoint touchPoint = [touch locationInView:self];
[self animateFirstTouchAtPoint:touchPoint];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
// If the touch was in the placardView, move the placardView to its location
if ([touch view] == placardView) {
CGPoint location = [touch locationInView:self];
placardView.center = location;
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
// If the touch was in the placardView, bounce it back to the center
if ([touch view] == placardView) {
// Disable user interaction so subsequent touches
// don't interfere with animation
self.userInteractionEnabled = NO;
[self animatePlacardViewToCenter];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// To impose as little impact on the device as possible, simply set
// the placard view's center and transformation to the original values
placardView.center = self.center;
placardView.transform = CGAffineTransformIdentity;
}
要确定多点触摸序列事件中最后一根手指什么时候离开视图对象,就要看传入的组合对象中的UITouch触摸对象数量,和传入的UIEvent事件对象中的UITouch触摸对象数量。如果这两个参数中的一致,也就意味着多点触摸序列事件结束了,如代码清单3-7:
Listing 3-7 Determining when the last touch in a multitouch sequence has ended
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if ([touches count] == [[event touchesForView:self] count]) {
// Last finger has lifted
}
}
需要记住一点,传入的set里的对象,对应的是和视图相关联、并在某个给定阶段中发生变化或者新加的触摸对象,然而方法touchesForView返回的对象,则是和视图相关联的所有的UITouch对象。也就是所有发生变化的触摸对象都出现在结束阶段的序列集合中时,也就是最后一根手指的触摸事件也到达了。
8.指定自定义触摸事件的行为
通过修改某个特定的手势识别器、某个特定的视图对象、或者APP中所有的触摸事件,来自定义APP处理触摸事件的方式。开发者可以通过一下几种方式,来修改对应的触摸事件流:
- 启用多点触摸事件的传输。
默认情况下,视图对象除了多点触摸的第一个触摸对象之外,其他的都会给忽略掉。如果开发者想要视图对象能响应处理多点触摸,就必须修改对应的属性来启用此功能,开发者可以在Interface Builder或者手工纯代码进行设置,将视图对象的multipleTouchEnabled属性设置为YES即可。
- 限制某个视图的事件传输。
默认情况下,视图对象的exclusiveTouch属性的值为NO,对应的意思是,此视图对象不会阻止窗体中的其他视图对象接收触摸事件对象。如果将指定的某个视图对应的这个值设置为YES,那么这个视图对象将会是仅有的唯一接收并追踪触摸事件的视图对象。
如果你的视图并非独有,用户可以一根手指在某个视图中点击、另一根手指在另一个视图操作,每个视图都有单独追踪各自的手势操作。
假如有视图设置如图3-5所示,视图A是一个独享触摸事件操作的视图对象。如果用户只在A中进行触摸操作,触摸操作就会被识别。但是假如用户先把手指按压在视图B中,然后又用另外一根手指在A中触摸,此时A是无法接收到触摸事件的,因为此时A视图不是唯一追踪触摸事件的视图对象。类似的,假如用户先用手指按压在A视图里,然后用另外一根手指去B中触摸操作,B就没有办法接收到触摸事件了,因为A是是唯一指定接收触摸事件的对象。其他任何情况下,用户对B和C同时操作,这两个视图都可以接收到各自的触摸事件对象。

- 限制子视图的事件传递。
通过重写自定义视图类的hitTest:withEvent:可以使得多点触摸事件不被传递给某个指定的子视图。请参阅“★ 重写Hit-Test拦截触摸事件”章节的描述。当然,开发者也可以直接完全关闭触摸事件的传递,或者关闭一段时间也可以
- 完全关闭触摸事件传递。
将试图对象的userInteractionEnabled属性为NO就可以关闭了。需要注意的是,如果某个视图被隐藏了或者纯透明状态下,也是无法接收触摸事件的。
- 在一段时间内关闭触摸事件传递。
有时候开发者可能只是想暂时地关闭掉触摸事件的传递,比如正在执行动画效果的过程时。此时,可以通过调用方法beginIgnoringInteractionEvent停止接收事件传递,然后在我们需要继续接收触摸事件时,调用方法endIgnoringInteractionEvent就可以了。
9.重写Hit-Test拦截触摸事件
如果开发者自定的视图对象包含有一些子视图对象的时候,就需要 确定好触摸事件的处理“是”在父视图层还是在子视图层。如果选择在父视图层处理,也就意味着所有的子视图对象不实现touchesBegan:withEvent, touchesEnded:withEvent, touchesMoved:withEvent等系列方法,与此同时,重写父视图类的hitTest:withEvent:方法并将其自己返回即可。
重写hit-testing,将其自身设置为hit-test的视图对象返回,可以确保 父视图对象 接收到 所有的触摸事件,因为父视图所截取并接收到的触摸对象,会优先被传递给其子视图对象进行处理。如果父视图对象没有重写hit-test的方法,触摸事件对象就会被绑定到事件所触发的子视图对象上,被子视图对象处理了,父视图对象也就接收不到了。
回想一下两个hit-test方法,一个是UIView视图对象的hitTest:withEvent:和CALayer对象的hitTest:,如章节“Hit-Testing返回触摸事件发生所在的视图对象”所描述。开发者几乎不需要自己调用这两个方法,更多的是重写这两个方法来拦截传递给子视图对象的触摸事件。然而,有时候响应器在事件转发之前进行hit-testing。
ps:我觉得UIView的hitTest底层其实就是layer的hitTest
10.转发触摸事件
要将触摸事件转发给另外一个响应器对象,只需要向其发送 触摸事件处理消息 即可。使用该技术时需要特别小心,因为UIKit框架的类对象的设计本身,是无法接收没有与其绑定的触摸事件对象的。如果某个响应器要处理触摸事件,触摸事件对应的视图对象就必须拥有此响应器的引用。如果开发者想要按照某些特定的条件或者规则,将事件转发给APP的其他响应器对象,那么这些响应器对象都必须是自己的定义的UIView子类的实例对象。
举个例子,某个APP有三个自定义视图对象:A,B,C。当用户触摸A时,APP的窗体就会通过hit-test检查到A,并将事件传递给A。假设在特定的条件下,A要么将此事件传递给B,要么就传递给C。这种情况下,A、B、C都必须清楚此转发动作,而且B、C必须能够处理这些没有和他们绑定的触摸事件。
事件转发通常需要对触摸对象进行分析,以确定它们是否应该被转发。有以下几种方法进行分析:
- 对于某个“覆盖图层”视图对象,比如常见的父视图,使用hit-testing来拦截触摸事件,从而进行分析是否需要将其转发给其子视图对象。
- 在自定的UIWindow子类中重写sendEvent:方法,分析触摸事件对象,然后将其转发给合适的响应器。
重写sendEvent:方法可以让开发者监控APP所接收到的所有事件。UIApplication和UIWindow都是在sendEvent方法中将接收到的事件进行派发,所以此方法就类似于所有事件进入APP的漏斗入口点一样。极少数的APP需要重写sendEvent方法,但是如果真的要重写的话,一定确保调用父类的实现—[super sendEvent:theEvent]。绝对不要篡改事件的分发处理。
代码清单3-8 展示了如何在UIWindow的一个子类中重写sendEvent。该示例代码中,事件对象被发送给自定义的帮助响应器, 在一个与之绑定的视图上执行线性转换动画。
Listing 3-8 Forwarding touch events to helper responder objects
- (void)sendEvent:(UIEvent *)event {
for (TransformGesture *gesture in transformGestures) {
// Collect all the touches you care about from the event
NSSet *touches = [gesture observedTouchesForEvent:event];
NSMutableSet *began = nil;
NSMutableSet *moved = nil;
NSMutableSet *ended = nil;
NSMutableSet *canceled = nil;
//Sort touches by phase to handle—-similar to normal event dispatch
for (UITouch *touch in touches) {
switch ([touch phase]) {
case UITouchPhaseBegan:
if (!began) began = [NSMutableSet set];
[began addObject:touch];
break;
case UITouchPhaseMoved:
if (!moved) moved = [NSMutableSet set];
[moved addObject:touch];
break;
case UITouchPhaseEnded:
if (!ended) ended = [NSMutableSet set];
[ended addObject:touch];
break;
case UITouchPhaseCancelled:
if (!canceled) canceled = [NSMutableSet set];
[canceled addObject:touch];
break;
default:
break;
}
}
// Call methods to handle the touches
if (began) [gesture touchesBegan:began withEvent:event];
if (moved) [gesture touchesMoved:moved withEvent:event];
if (ended) [gesture touchesEnded:ended withEvent:event];
if (canceled) [gesture touchesCancelled:canceled withEvent:event];
}
[super sendEvent:event];
}
注意,子类重写的sendEvent方法中调用了父类的实现,这对于整个触摸事件流的完整性非常重要。
11.处理多点触摸事件的最佳实践
在处理触摸和运动事件的时候,开发者应该遵循以下几个推荐的技术和模式:
1、记得实现事件取消方法。
在取消方法的实现中,将试图对象的状态恢复成事件发生之前的初始状态。如果不这么做的话,视图对象将会处于一个变形的状态下,在某些情况下,可能会导致其他视图对象接收到此取消事件的消息。
2、如果开发者在UIView,UIViewController或者UIRespondor的子类中处理事件:
- 实现所有的事件处理方法,即使这些方法什么都不做。
- 不要在这些方法中调用父类的实现。
3、如果是在其他UIKit框架响应器类的子类中处理事件:
在这里可不需要实现所有的事件处理方法。
在这些自定义实现的方法中,确保调用父类的实现,举个例子:
[super touchesBegan:touches withEvent:event];
4、不要将事件转发给任何不属于UIKit框架的响应器对象。
事件转发,应该转发给UIView的子类对象,这些响应器实例对象。另外,确保这些响应器对象清楚地知道,事件转发正在进行,同时它们可以接收到这些没有与之绑定的触摸事件。
5、不要显示地调用nextResponder方法将事件沿着响应器链向上传递,而是调用父类的实现,让UIKit框架来处理响应器链的事件传递。
6、不要在事件处理过程中使用“约数转换整数”的代码,这会导致精度丢失。
为了维持代码良好的兼容性,iOS所传达的触摸事件在一个320x480的坐标系空间内。然而,在高分辨率的设备上,对应的分辨率刚好为此分辨率的两倍:640x960.这也就意味着,触摸事件在高分辨率的设备上以半个坐标点为基点(即一个像素点),而在一些老的设备上则是整个坐标点为基点(也是一个像素点)。
运动事件
当用户移动、摇晃或者倾斜设备时,就会产生运动事件。这些运动事件是设备上的硬件检测到的,也就是我们所说的加速器(accelerometer) 和陀螺仪(accelerometer) 。
加速器 实际上是由三个加速器组成的,x轴、y轴和z轴。每一个加速器都会在直线方向上实时的计算各自轴向上的向量变化。将这三个加速器组合在一起,就可以检测到设备任何方向上的运动,并获得设备的当前方位。尽管这是三个不同的加速器,但是本文将这三个组合定义为一个完整的加速器实体。陀螺仪则用来检测在x,y,z三个方向上的旋转角度。
所有的运动事件都源自于相同的硬件。有以下几个方法可以获取硬件信息数据,使用哪一个则取决于APP的需求:
- 如果只需要检测常用的设备方位信息,而不需要方位信息的向量,可以使用UIDevice类。更多信息请参见章节“使用UIDevice获取当前设备方位”。
- 如果想要APP响应用户摇晃设备的事件,可以使用UIKit框架的运动事件处理方法,从传入的UIEvent对象中获取相应的信息。更多相关信息请参见章节“使用UIEvent检测摇晃运动事件”。
- 如果UIDevice和UIEvent还觉得不够,那就使用Core Motion框架直接访问加速器、陀螺仪和设备运动类。更多信息请参见章节“使用Core Motion捕获设备动作”。
后面的往后再看吧

本文详细介绍了iOS中的事件传递机制,包括事件响应链、hit-testing和多点触摸事件的处理。事件传递从UIApplication开始,沿着响应链直到找到合适的处理对象。在多点触摸事件中,通过创建UIResponder子类并实现触摸事件处理方法来追踪和响应触摸事件。文章还探讨了如何重写hit-test以拦截触摸事件和转发触摸事件给其他响应器对象。
728

被折叠的 条评论
为什么被折叠?



