事件的传递
系统将事件按照特定的路径传递给可以对其进行处理的对象。如“核心应用程序架构”部分描述的那样,当用户触摸设备屏幕时,iPhone OS会将它识别为一组触摸对象,并将它们封装在一个UIEvent
对象中,放入当前应用程序的事件队列中。事件对象将特定时刻的多点触摸序列封装为一些触摸对象。负责管理应用程序的UIApplication
单件对象将事件从队列的顶部取出,然后派发给其它对象进行处理。典型情况下,它会将事件发送给应用程序的键盘焦点窗口—即拥有当前用户事件焦点的窗口,然后代表该窗口的UIWindow
对象再将它发送给第一响应者进行处理(第一响应者在
应用程序通过触碰测试(hit-testing)来寻找事件的第一响应者,即通过递归调用视图层次中视图对象的hitTest:withEvent:
方法来确认发生触摸的子视图。触摸对象的整个生命周期都和该视图互相关联,即使触摸动作最终移动到该视图区域之外也是如此。“事件处理技巧”部分对触碰测试在编程方面的一些隐含意义进行讨论。
UIApplication
对象和每个UIWindow
对象都在sendEvent:
方法(两个类都声明了这个方法)中派发事件。由于这些方法是事件进入应用程序的通道,所以,您可以从UIApplication
或UIWindow
派生出子类,重载其sendEvent:
方法,实现对事件的监控或执行特殊的事件处理。但是,大多数应用程序都不需要这样做。
响应者对象和响应者链
响应者对象是可以响应事件并对其进行处理的对象。UIResponder
是所有响应者对象的基类,它不仅为事件处理,而且也为常见的响应者行为定义编程接口。UIApplication
、UIView
、和所有从UIView
派生出来的UIKit类(包括UIWindow
)都直接或间接地继承自UIResponder
类。
第一响应者是应用程序中当前负责接收触摸事件的响应者对象(通常是一个UIView
对象)。UIWindow
对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。
响应者链是一系列链接在一起的响应者对象,它允许响应者对象将处理事件的责任传递给其它更高级别的对象。随着应用程序寻找能够处理事件的对象,事件就在响应者链中向上传递。响应者链由一系列“下一个响应者”组成,其顺序如下:
-
第一响应者将事件传递给它的视图控制器(如果有的话),然后是它的父视图。
-
类似地,视图层次中的每个后续视图都首先传递给它的视图控制器(如果有的话),然后是它的父视图。
- 最上层的容器视图将事件传递给
UIWindow
对象。 -
UIWindow
对象将事件传递给UIApplication
单件对象。
如果应用程序找不到能够处理事件的响应者对象,则丢弃该事件。
响应者链中的所有响应者对象都可以实现UIResponder
的某个事件处理方法,因此也都可以接收事件消息。但是,它们可能不愿处理或只是部分处理某些事件。如果是那样的话,它们可以将事件消息转送给下一个响应者,方法大致如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { |
UITouch* touch = [touches anyObject]; |
NSUInteger numTaps = [touch tapCount]; |
if (numTaps < 2) { |
[self.nextResponder touchesBegan:touches withEvent:event]; |
} else { |
[self handleDoubleTap:touch]; |
} |
} |
请注意:如果一个响应者对象将一个多点触摸序列的初始阶段的事件处理消息转发给下一个响应者(在touchesBegan:withEvent:
方法中), 就应该同样转发该序列的其它事件处理消息。
动作消息的处理也使用响应者链。当用户对诸如按键或分页控件这样的UIControl
对象进行操作时,控件对象(如果正确配置的话)会向目标对象发送动作消息。但是,如果目标对象被指定为nil
,应用程序就会像处理事件消息那样,把该动作消息路由给第一响应者。如果第一响应者没有进行处理,再发送给其下一个响应者,以此类推,将消息沿着响应者链向上传递。
调整事件的传递
UIKit为应用程序提供了一些简化事件处理、甚至完全关闭事件流的编程接口。下面对这些方法进行总结:
-
关闭事件的传递。缺省情况下,视图会接收触摸事件。但是,您可以将其
userInteractionEnabled
属性声明设置为NO
,关闭事件传递的功能。隐藏或透明的视图也不能接收事件。 -
在一定的时间内关闭事件的传递。应用程序可以调用
UIApplication
的beginIgnoringInteraction
方法,并在随后调用Events endIgnoringInteractionEv
方法来实现这个目的。前一个方法使应用程序完全停止接收触摸事件消息,第二个方法则重启消息的接收。某些时候,当您的代码正在执行动画时,可能希望关闭事件的传递。ents -
打开多点触摸的传递。
缺省情况下,视图只接收多点触摸序列的第一个触摸事件,而忽略所有其它事件。如果您希望视图处理多点触摸,就必须使它启用这个功能。在代码或Interface Builder的查看器窗口中将视图的 multipleTouchEnabled
属性设置为YES
,就可以实现这个目标。 -
将事件传递限制在某个单独的视图上。
缺省情况下,视图的 exclusiveTouch
属性被设置为NO
。将这个属性设置为YES
会 使相应的视图具有这样的特性:即当该视图正在跟踪触摸动作时,窗口中的其它视图无法同时进行跟踪,它们不能接收到那些触摸事件。然而,一个标识为“独占触 摸”的视图不能接收与同一窗口中其它视图相关联的触摸事件。如果一个手指接触到一个独占触摸的视图,则仅当该视图是窗口中唯一一个跟踪手指的视图时,触摸 事件才会被传递。如果一个手指接触到一个非独占触摸的视图,则仅当窗口中没有其它独占触摸视图跟踪手指时,该触摸事件才会被传递。 -
将事件传递限制在子视图上。一个定制的
UIView
类可以通过重载hitTest:withEvent:
方法来将多点触摸事件的传递限制在它的子视图上。这个技巧的讨论请参见“事件处理技巧”部分。
处理多点触摸事件
为了处理多点触摸事件,UIView
的定制子类(比较不常见的还有UIApplication
或UIWindow
的定制子类)必须至少实现一个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 |
在给定的触摸阶段中,如果发生新的触摸动作或已有的触摸动作发生变化,应用程序就会发送这些消息:
-
当一个或多个手指触碰屏幕时,发送
touchesBegan:withEvent:
消息。 -
当一个或多个手指在屏幕上移动时,发送
touchesMoved:withEvent:
消息。 -
当一个或多个手指离开屏幕时,发送
touchesEnded:withEvent:
消息。 -
当触摸序列被诸如电话呼入这样的系统事件所取消时,发送
touchesCancelled:withEvent:
消息。
上面这些方法都和特定的触摸阶段(比如UITouchPhaseBegan
)相关联,该信息存在于UITouch
对象的phase
属性声明中。
每个与事件处理方法相关联的消息都有两个参数。第一个参数是一个UITouch
对象的集合,表示给定阶段中新的或者发生变化的触摸动作;第二个参数是一个UIEvent
对象,表示这个特定的事件。您可以通过这个事件对象得到与之相关联的所有触摸对象(allTouches
),或者发生在特定的视图或窗口上的触摸对象子集。其中的某些触摸对象表示自上次事件消息以来没有发生变化,或虽然发生变化但处于不同阶段的触摸动作。
为了处理给定阶段的事件,响应者对象常常从传入的集合参数中取得一或多个UITouch
对象,然后考察这些对象的属性或取得它们的位置(如果需要处理所有触摸对象,可以向该NSSet
对象发送anyObject
消息)。UITouch
类中有一个名为locationInView:
的重要方法,如果传入self
参数值,它会给出触摸动作在响应者坐标系统中的位置(假定该响应者是一个UIView
对象,且传入的视图参数不为nil
)。另外,还有一个与之平行的方法,可以给出触摸动作之前位置(previousLocationInView:
)。UITouch
实例的属性还可以给出发生多少次触碰(tapCount
)、触摸对象的创建或最后一次变化发生在什么时间(timestamp
)、以及触摸处于什么阶段(phase
)。
响应者类并不是必须实现上面列出的所有三个事件方法。举例来说,如果它只对手指离开屏幕感兴趣,则只需要实现touchesEnded:withEvent:
方法就可以了。
在一个多点触摸序列中,如果响应者在处理事件时创建了某些持久对象,则应该实现touchesCancelled:withEvent:
方法,以便当系统取消该序列的时候对其进行清理。多点触摸序列的取消常常发生在应用程序的事件处理遭到外部事件—比如电话呼入—破坏的时候。请注意,响应者对象同样应该在收到多点触摸序列的touchesEnded:withEvent:
消息时清理之前创建的对象(“事件处理技巧”部分讨论了如何确定一个序列中的最后一个touch-up事件)。
处理单个和多个触碰手势
iPhone应用程序中一个很常见的手势是触击:即用户用手指触碰一个对象。响应者对象可以以一种方式响应单击,而以另外一种方式响应双击,甚至可能以第三种方式响应三次触击。您可以通过考察UITouch
对象的tapCount
属性声明值来确定用户在一个响应者对象上的触击次数,
取得这个值的最好地方是touchesBegan:withEvent:
和touchesEnded:withEvent:
方法。在很多情况下,我们更倾向于后者,因为它与用户手指离开屏幕的阶段相对应。在触摸结束阶段(UITouchPhaseEnded
)考察触击的次数可以确定手指是真的触击,而不是其它动作,比如手指接触屏幕后拖动的动作。
程序清单3-1展示了如何检测某个视图上是否发生双击。
程序清单3-1
- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event |
{ |
UITouch *touch = [touches anyObject]; |
|
if ([touch tapCount] == 2) { |
CGPoint tapPoint = [theTouch locationInView:self]; |
// Process a double-tap gesture |
} |
} |
当一个响应者对象希望以不同的方式响应单击和双击事件时,就会出现复杂的情况。举例来说,单击的结果可能是选定一个对象,而双击则可能 是显示一个编辑视图,用于编辑被双击的对象。那么,响应者对象如何知道一个单击不是另一个双击的起始部分呢?我们接下来解释响应者对象如何借助上文刚刚描 述的事件处理方法来处理这种情况:
-
在
touchesEnded:withEvent:
方法中,当触击次数为一时,响应者对象就向自身发送一个performSelector:withObject:afterDelay:
消息,其中的选择器标识由响应者对象实现的、用于处理单击手势的方法;第二个参数是一个NSValue
或NSDictionary
对象,用于保存相关的UITouch
对象;时延参数则表示单击和双击手势之间的合理时间间隔。 -
在
touchesBegan:withEvent:
方法中,如果触击次数为二,响应者对象会向自身发送一个cancelPreviousPerformReq
消息,取消当前被挂起和延期执行的调用。如果触碰次数不为二,则在指定的延时之后,先前步骤中由选择器标识的方法就会被调用,以处理单击手势。uestsWithTarget: -
在
touchesEnded:withEvent:
方法中,如果触碰次数为二,响应者会执行处理双击手势的代码。检测碰擦手势
水平和垂直的碰擦(Swipe)是简单的手势类型,您可以简单地在自己的代码中进行跟踪,并通过它们执行某些动作。为了检测碰擦手势,您需要跟踪用户手指 在期望的坐标轴方向上的运动。碰擦手势如何形成是由您自己来决定的,也就是说,您需要确定用户手指移动的距离是否足够长,移动的轨迹是否足够直,还有移动 的速度是否足够快。您可以保存初始的触碰位置,并将它和后续的touch-moved事件报告的位置进行比较,进而做出这些判断。
程序清单3-2展示了一些基本的跟踪方法,可以用于检测某个视图上发生的水平碰擦。在这个例子中,视图将触摸的初始位置存储在名为
startTouchPosition
的 成员变量中。随着用户手指的移动,清单中的代码将当前的触摸位置和起始位置进行比较,确定是否为碰擦手势。如果触摸在垂直方向上移动得太远,就会被认为不 是碰擦手势,并以不同的方式进行处理。但是,如果手指继续在水平方向上移动,代码就继续将它作为碰擦手势来处理。一旦碰擦手势在水平方向移动得足够远,以 至于可以认为是完整的手势时,处理例程就会触发相应的动作。检测垂直方向上的碰擦手势可以用类似的代码,只是把x和y方向的计算互换一下就可以了。程序清单3-2
在视图中跟踪碰擦手势 #define HORIZ_SWIPE_DRAG_MIN 12
#define VERT_SWIPE_DRAG_MAX 4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
startTouchPosition = [touch locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self];
// If the swipe tracks correctly.
if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
{
// It appears to be a swipe.
if (startTouchPosition.x < currentTouchPosition.x)
[self myProcessRightSwipe:touches withEvent:event];
else
[self myProcessLeftSwipe:touches withEvent:event];
}
else
{
// Process a non-swipe event.
}
}
处理复杂的多点触摸序列
触击和碰擦是简单的手势。如何处理更为复杂的多点触摸序列—实际上是解析应用程序特有的手势—取决于应用程序希望完成的具体目标。您可以跟踪所有阶段的所有触摸动作,记录触摸对象中发生变化的属性变量,并正确地改变内部的状态。
说明如何处理复杂的多点触摸序列的最好方法是通过实例。程序清单3-3展示一个定制的
UIView
对象如何通过在屏幕上动画移动“Welcome”标语牌来响应用户手指的移动,以及如何通过改变欢迎标语的语言来响应用户的双击手势(例子中的代码来自一个名为MoveMe的示例工程,进一步考察该工程可以更好地理解事件处理的上下文)。程序清单3-3
处理复杂的多点触摸序列 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
// Only move the placard view if the touch was in the placard view
if ([touch view] != placardView) {
// On double tap outside placard view, update placard's display string
if ([touch tapCount] == 2) {
[placardView setupNextDisplayString];
}
return;
}
// "Pulse" the placard view by scaling up then down
// Use UIView's built-in animation
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
CGAffineTransform transform = CGAffineTransformMakeSca
le(1.2, 1.2); placardView.transform = transform;
[UIView commitAnimations];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
transform = CGAffineTransformMakeSca
le(1.1, 1.1); placardView.transform = transform;
[UIView commitAnimations];
// Move the placardView to under the touch
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
placardView.center = [self convertPoint:[touch locationInView:self] fromView:placardView];
[UIView commitAnimations];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
// If the touch was in the placardView, move the placardView to its location
if ([touch view] == placardView) {
CGPoint location = [touch locationInView:self];
location = [self convertPoint:location fromView:placardView];
placardView.center = location;
return;
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] 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 animatePlacardViewToCent
er]; return;
}
}
请注意:对于通过描画自身的外观来响应事件的定制视图,在事件处理方法中通常应该只是设置描画状态,而在
drawRect:
方法中执行所有的描画操作。如果需要了解更多关于描画视图内容的方法,请参见“图形和描画”部分。事件处理技巧
下面是一些事件处理技巧,您可以在自己的代码中使用。
-
跟踪UITouch对象的变化
在事件处理代码中,您可以将触摸状态的相关位置保存下来,以便在必要时和变化之后的
UITouch
实例进行比较。作为例子,假定您希望将每个触摸对象的最后位置和其初始位置进行比较,则在touchesBegan:withEvent:
方法中,您可以通过locationInView:
方法得到每个触摸对象的初始位置,并以UITouch
对象的地址作为键,将它们存储在CFDictionaryRef
封装类型中;然后,在touchesEnded:withEvent:
方法中,可以通过传入UITouch
对象的地址取得该对象的初始位置,并将它和当前位置进行比较(您应该使用CFDictionaryRef
类型,而不是NSDictionary
对象,因为后者需要对其存储的项目进行拷贝,而UITouch
类并不采纳NSCopying
协议,该协议在对象拷贝过程中是必须的)。 -
对子视图或层上的触摸动作进行触碰测试
定制视图可以用
UIView
的hitTest:withEvent:
方法或CALayer
的hitTest:
方法来寻找接收触摸事件的子视图或层,进而正确地处理事件。下面的例子用于检测定制视图的层中的“Info” 图像是否被触碰。- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
CGPoint location = [[touches anyObject] locationInView:self];
CALayer *hitLayer = [[self layer] hitTest:[self convertPoint:location fromView:nil]];
if (hitLayer == infoImage) {
[self displayInfo];
}
}
如果您有一个携带子视图的定制视图,就需要明确自己是希望在子视图的级别上处理触摸事件,还是在父视图的级别上进行处理。如果子视图没有实现
touchesBegan:withEvent:
、touchesEnded:withEvent:
、或者touchesMoved:withEvent:
方法,则这些消息就会沿着响应者链被传播到父视图。然而,由于多次触碰和多点触摸事件与发生这些动作所在的子视图是互相关联的,所以父视图不会接收到这些事件。为了保证能接收到所有的触摸事件,父视图必须重载hitTest:withEvent:
方法,并在其中返回其本身,而不是它的子视图。 -
确定多点触摸序列中最后一个手指何时离开
当您希望知道一个多点触摸序列中的最后一个手指何时从视图离开时,可以将传入的集合参数中包含的
UITouch
对象数量和UIEvent
参数对象中与该视图关联的触摸对象数量相比较。请看下面的例子:- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if ([touches count] == [[event touchesForView:self] count]) {
// last finger has lifted....
}
}