“手势”(gesture)是指从你用一个或多个手指接触屏幕时开始,直到你的手指离开屏幕为止所发生的所有事件。手势在事件中传递到系统。事件将在用户与iPhone的多触摸屏幕交互时生成,其中包含一个或多个触摸的信息
“触摸”(touch)是指手指放到iPhone的屏幕上。手势中涉及的触摸数量等于同时位于屏幕上的手指数量。实际上,你可以将所有5个手指都放到屏幕上,这要这些手指彼此不要靠太近,iPhone就能够识别并跟踪所有的手指。
“轻击”(tap)用一个手指触摸屏幕,然后立即离开屏幕(而不是来回移动)。iPhone跟踪轻击的数量,并且可以告诉你用户轻击了两次还是3次,甚至是20次。例如,他处理所有计时以及其他必要的工作来区分两个单击还是一次双击。iPhone只跟踪使用一个手指时的轻击,如果他检测到多个触摸,则会将轻击计数重置为1.
响应者链
由于手势是在事件之内传递到系统的,然后事件会传递到响应者链(responder chain),因此你需要了解响应者链的工作方式,以便正确处理手势。
本书中,曾多次提到过第一响应者,该响应者通常是用户当前正在交互的对象。第一响应者是响应者链的开始,还有其他响应者。
让UIResponder做为其超类之一的任何类都是响应者。UIView是UIResponder的子类,UIControl是UIView的子类,所以所有视图和所有控件都是响应者。UIViewController也是UIResponder的子类,这意味着他是响应者,其所有子类也都是响应者。
如果第一个响应者不处理某个特殊事件,则他会将该事件传递到响应者链的下一级。如果该链中的下一个对象响应此特殊事件,则他通常会处理该事件,这将停止该事件沿着响应者链向前传递。在某些情况下,如果某个响应者只对某个事件进行部分处理,则该响应者将采取操作,并将该事件转发给链中的下一个响应者。但通常不会发生这种情况。正常情况下,当对象响应事件时,即到达了该事件的行尾。如果事件通过整个响应者链并且没有对象处理该事件,则丢弃该事件。
下面让我们再更具体地看一下响应者链。第一个响应者几乎总是视图或控件,并且首先对事件进行响应。如果第一个响应者不处理该事件,则他会将该事件传递给其视图控制器。如果此视图控制器不处理该事件,则将该事件传递给第一个响应者的父视图。如果父视图没有响应,则该事件将转到父视图的控制器(如果有)。该事件将沿着每个视图的视图层次结构继续前进,然后该视图的控制器获得处理该事件的机会。如果该事件一直通过视图层次结构,则会将该事件传递给应用程序的窗口。如果窗口不处理该事件,则该窗口会将该事件传递给应用程序的对象实例UIApplication。如果UIApplication不响应该事件,则该事件逐渐进入睡眠状态。
这个过程非常重要,这有多个原因。首先他控制可以处理手势的方式。比如说,一个用户正在查看某个表,他用某个手指轻扫该表的某一行。哪个对象会处理该手势呢?
如果是在某个视图或控件之内轻扫,而该视图或控件是表视图单元的子视图,则该视图或控件将获得响应的机会。如果他没有响应,则表视图单元会获得机会。在某个应用程序中,可以使用轻扫操作来删除某个邮件,表视图单元可能需要查看该事件,看他是否包含轻扫手势。但是大多数表视图单元不响应手势,如果他们不响应,则该事件将继续通过表视图,然后通过其他响应者,知道某些内容响应该事件或者到达行的结尾为止。
转发事件:保持响应者链的活动状态
让我们回到邮件应用程序中的表视图单元。我们不知道苹果公司邮件应用程序的内部细节,但是我们暂且可以认为表视图单元支持轻扫式删除且仅支持轻扫式删除。该表视图单元必须实现与接收触摸事件相关的方法,以便他可以进行检查,看该事件是否包含轻扫手势。如果该事件包含轻扫,则表视图单元会采取操作,该事件将停在传递。
如果该事件不包含轻扫手势,则表视图单元负责将该事件手动转发给响应者链中的下一个对象。如果他没有进行转发的工作,则表和链上的其他对象将永远也不会获得响应的机会,并且该应用程序可能无法如用户所期望地正常工作。该表视图单元可能会阻止其他视图识别手势。
只要你响应触摸事件,就必须记住代码无法在真空中工作。如果某个对象截获某个他无法处理的事件,则也需要通过在下一个响应者上调用相同的方法来手动传递该对象。下面是其中一小部分代码:
- (void)respondToFictionalEvent:(UIEvent *)event {
if (someCondition)
[self handleEvent:event]
else
[self.nextResponder respondToFictionalEvent:event];
}
确保将事件推回到响应者链中,这一点非常重要。
多触摸体系结构
一般来说,处理手势的事件的代码可以放在UIView的子类中,也可以放在UIView Controller中。那么该代码属于视图还是视图控制器?
如果视图需要根据用户的触摸来对自己执行某些操作,则代码可能属于定义该视图的类。例如,很多控件类(如UISwitch和UISlider)都响应与触摸有关的事件。
但是,通常当正在处理的手势影响正在触摸的多个对象时,该手势代码才真正属于视图的控制器类。例如,如果用户对一行进行手势触摸,该触摸指出应该删除所有行,则应该由视图控制器中的代码来处理该手势。
4个手势通知方法
1)当用户第一次触摸屏幕时,iPhone将查找touchesBegan:withEvent:方法的响应者。若要查清用户第一次开始进行手势或轻击屏幕的时间,请在你的视图或视图控制器中实现该方法:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSUInteger numTaps=[[touches anyObject] tapCount];
NSUInteger numTouches=[touches count];
...
}
该方法以及所有与触摸有关的方法都传递到一个NSSet实例和一个UIEvent实例。
[touches count] 获取手指数量。touches中的每个对象都是一个UITouch事件,该事件表示一个手指正在触摸屏幕。如果该触摸是一些列轻击的一部分,则可以通过询问任何UITouch对象来查询轻击数量。当然,如果触摸中有多个对象,则轻击计数必将为1,因为只有只使用一个手指来轻击屏幕时,系统才保留轻击计数。在上面的示例中如果numTouches为2,则用户只是轻击屏幕两次。
touches中的所有对象都可能与你实现该方法的视图或视图控制器无关。例如,表视图单元可能并不关心其他行中的触摸或者导航栏中的触摸。你可以从事件中获得一个子集,他仅拥有位于特殊视图中的触摸的touches,如下所示:
NSSet *myTouches=[event touchesForView:self.view];
每个UITouch都表示不同的手指,并且每个手指都位于屏幕上的不同位置。你可以使用UITouch对象查询特定手指的位置。如果需要,甚至可以将点转换为视图的本地坐标系,如下所示:
CGPoint point=[touch locationInView:self];
2)当用户将手指移过屏幕时,你可以通过实现touchesMoved:withEvent:来获得通知。
在长时间的拖动过程中会多次调用该方法,并且每次调用该方法时,将获得另一组触摸以及另一个事件。除了能够从UITouch对象获得每个手指的当前位置之外,还可以查清该触摸的原来的位置,这是上次调用touchesMoved:withEvent:或
touchesBegan:withEvent:时手指的位置
3)当用户的手指离开屏幕时,会调用另一个事件,即touchesEnded:withEvent:。调用此方法时,你知道用户是在使用手势。
4)最后一个与触摸有关的方法是touchesCancelled:withEvent:。当发生某些事件导致手势中断时会调用该方法。可以在此处进行任何所需的清理工作,以便你可以重新开始一个新手势。调用该方法时,对于当前手势,将不会调用touchesEnded:withEvent:。
在Xcode中,使用基于视图的应用程序模版创建新项目,并调用新项目TouchExplorer。每次调用与触摸有关的方法时,TouchExplorer都会将消息打印到屏幕中,包含触摸和轻击计数。
我们需要为该应用程序提供3个标签:一个用于指示最后调用的方法,一个用于报告当前轻击计数,一个用于报告触摸数量。
单击TouchExplorerViewControl
IBOutlet UILabel *messageLabel;
IBOutlet UILabel *tapsLabel;
IBOutlet UILabel *touchesLabel;
...
- (void)updateLabelsFromTouches:(NSSet *)touches;
@end
说明:在仿真器上运行该应用程序,你无法看到所有可用的多触摸功能,除非你在iPhone或iPad上运行。
现在,双击TouchExplorerViewControl
最后,单击View图标,Cmd+1打开属性检查器,在检查器中,确保同时选中User Interacting Enabled和Mutiple Touch。如果未选
中Mutiple Touch,你的控制器类的触摸方法将始终接受一个并且只能接受一个触摸。
然后单击TouchExplorerViewControl
- (void)updateLabelsFromTouches:(NSSet *)touches
{
NSUInteger numTaps=[[touches anyObject] tapCount];
NSString *tapsMessage=[[NSString alloc] initWithFormat:@"%d taps detected",numTaps];
tapsLabel.text=tapsMessage;
[tapsMessage release];
NSUInteger numTouches=[touches count];
NSString *touchMsg=[[NSString alloc] initWithFormat:@"%d touches detected",numTouches];
touchesLabel.text=touchMsg;
[touchMsg release];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
messageLabel.text=@"Touches Began";
[self updateLabesFromTouches:touches];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
messageLabel.text=@"Touches Cancelled";
[self updateLabesFromTouches:touches];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
messageLabel.text=@"Touches Ended";
[self updateLabesFromTouches:touches];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
messageLabel.text=@"Touches Moved";
[self updateLabesFromTouches:touches];
}
如果是在仿真器中运行应用程序,则尝试重复单击屏幕来提高轻击计数,尝试在视图周围单击和在按住鼠标的情况下拖动鼠标来模拟触摸和拖动。可以在用鼠标单击并进行拖动时按下Option键,以此在仿真器中模仿两个手指捏合的手势。
还可以这样来模拟两个手指的轻扫手势:首先按下Option键来模仿两个手指捏合,然后移动鼠标以便表示虚拟手指的两个点彼此相互靠近,然后再按下Shift键(同时仍然按着Option键)。按shift键将锁定两个手指相对于彼此的位置,并且可以进行轻扫和其他两个手指的手势。你将无法进行需要3个或多个手指的手势,但你可以在仿真器上使用Option键和shift键的组合来进行大多数两个手指的手势。
完成之后,让我们看看如何检测最常用的手势之一,即轻扫。
Swipe应用程序
再次创建一个基于视图的应用程序模版的应用程序-Swipes。我们将要构建的应用程序就是为了检测水平和垂直轻扫。
检测轻扫操作相对来说比较容易。我们将以像素为单位定义最小手势长度,即将该手势算作轻扫之前,用户必须轻扫的长度。我们还将定义一个偏差,即用户可以从某条直线偏离多远,仍然可以将该手势算作水平或垂直轻扫。通常不会将对角线算作轻扫,但是如果只是与水平或垂直轻扫偏离一点,也会将其当成轻扫。
当用户触摸屏幕时,我们将第一次触摸的位置保存在变量中,然后,当用户手指移动着通过屏幕时,我们将进行检查,看他是否达到某个点,这个点足够远且足够直,以便能够算作轻扫。
单击SwipesViewController.h:
#define kMinimumGestureLength 25
#define kMaximumVariance 5
...
IBOutet UILabel *label;
CGPoint gestureStartPoint;
- (void)eraseText;
然后设置SwipesViewController.xib,添加Label和设置View视图设置为接收多个触摸。
单击SwipesViewController.m:
- (void)eraseText
{
label.text=@"";
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
getstureStartPoint=[touch locationInView:self.view];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch=[touches anyObject];
CGPoint currentPosition=[touch locationInView:self.view];
CGFloat deltaX=fabsf(gestureStartPoint.x-currentPosition.x);
CGFloat deltaY=fbasf(gestureStartPoint.y-currentPosition.y);
if (deltaX >=kMinimumGestureLength && deltaY <=kMaximumVariance) {
label.text=@"Horizontal swipe detected";
[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
}
else if (deltaY >=kMinimumGestureLength && deltaX <= kMaximumVariance ) {
label.text=@"Vertical swipe detected";
[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
}
}
@end
在Swipes应用程序中,我们仅关心一个手指的轻扫,因此我们只从触摸集中获取某个对象来计算出在轻扫期间用户手指的位置。
但是,如果我们想实现两个或3个手指的轻扫,则会遇到一点问题。这个问题就是:所提供的触摸将作为NSSet,而不是NSArray。这些集是没有顺序的集合,这意味着,当我们进行比较时,没有简单的方法来计算出哪个手势使用了哪个手指。例如,我们不能假设该集中的第一次触摸所使用的手指是否与该手势开始后该集中的第一次触摸所使用的手指相同。
更糟糕的是,当用户执行两个或3个手指的手势时,完全有可能一个手指比另一个手指先触摸屏幕,这意味着在touchesBegan方法中,我们可能只能获得有关一个触摸的信息。
我们需要一种方法来检测多个手指的轻扫,而不会将其他手势(如捏合)错误识别为轻扫。解决方法非常简单。当touchesBegan通知某个手势已经开始时,我们保存一个手指的位置,就像我们之前做的那样。
在检测轻扫时,我们遍历向touchesMoved方法提供的所有触摸,从而将每个触摸与已保存的点相比较。如果用户进行的是多手指轻扫,则当与保存的点进行比较时,至少我们在该方法中获得的其中一个触摸将指示轻扫。如果我们发现水平轻扫或垂直轻扫,则再次遍历触摸,并确保每个手指至少与第一个手指的水平和垂直位置保持最小距离。
现在让我们对Swipes应用程序进行改进,以便检测多手指轻扫。要实现此目的,我们需要对头文件进行少量修改。因此,单击SwipesViewController.h:
声明一个枚举:
typedef enum {
kNoSwipe=0,
kHorizontalSwipe,
kVerticalSwipe
} SwipeType;
此枚举将提供一种方法,指出某个手势是水平轻扫还是垂直轻扫,或者根本未检测出轻扫。现在切换到SwipesViewController.m:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
SwipeType swipeType=kNowSwipe;
for (UITouch *touch in touches) {
CGPoint currentPosition=[touch locationInView:self.view];
CGFloat deltaX=fabsf(gestureStartPoint.x-currentPosition.x);
CGFloat deltaY=fbasf(gestureStartPoint.y-currentPosition.y);
if (deltaX >=kMinimumGestureLength && deltaY <=kMaximumVariance) {
swipeType=kHorizontalSwipe;
}
else if (deltaY >=kMinimumGestureLength && deltaX <= kMaximumVariance ) {
swipeType=kVerticalSwipe;
}
}
BOOL allFingersFarEnoughAway=YES;
if (swipeType !=kNowSwipe) {
for (UITouch *touch in touches) {
CGPoint currentPosition=[touch locationInView:self.view];
CGFloat distance;
if (swipeType == kHorizontalSwipe)
distance=fbasf(currentPosition.x-gestureStartPoint.x);
else
distance=fbasf(currentPosition.y-gestureStartPoint.y);
if (distance<kMinimumGestureLength)
allFingerFarEnoughAway=NO;
}
}
if (allFingersFarEnoughAway && swipeType != kNoSwipe)
{
NSString *swipeCountString=nil;
if ([touches count]==2)
swipeCountString=@"Double";
else if ([touches count==3])
swipeCountString=@"Triple";
else if ([touches count]==4)
swipeCountString=@"Quadruple";
else
swipeCountString=@"";
NSString *swipeTypeString=(swipeType==kHorizontalSwipe)?@"Horizontal":@"Vertical";
NSString *message=[[NSString alloc] initWithFormat:@"%@%@ Swipe Detected.",swipeCountString,swipeTypeString];
label.text=message;
[message release];
[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
}
}
如果你想对两次轻击执行某些完全不同于3次轻击的操作,拥有3个单独的通知可能会引起某个问题。让我们创建另一个应用程序进行演示并解决该问题。
在Xcode中,使用基于视图的应用程序模版创建一个新的项目。命名为TapTaps。该应用程序将拥有4个标签,当他检测到轻击1次、轻击两次、轻击3次以及轻击4次时,他们会分别通知我们。在第一个版本的此应用程序中,所有4个字段将单独工作,因此如果轻击4次,你会得到所有4个轻击类型。
此第一个版本运行之后,我们将查看如何更改他的行为,以便当用户停止轻击时只出现一个标签,该标签显示用户轻击的总数。
我们需要为4个标签提供输出口,并且我们还需要为每个轻击方案提供单独的方法,以便模拟你在实际应用程序中拥有的内容。还包括一个擦除文本字段的方法。展开Classes文件夹,单击TapTapsViewController.h:
IBOutlet UILabel *singleLabel;
IBOutlet UILabel *doubleLabel;
IBOutlet UILabel *tripleLabel;
IBOutlet UILabel *quadrupleLabel;
- (void)singleTap;
- (void)doubleTap;
- (void)tripleTap;
- (void)quadrupleTap;
- (void)eraseMe:(UITextField *)textField;
然后打开TapTapsViewController.xib,从库中拖出4个Labels。设置输出口,设置视图设置接受多触摸。
TapTapsViewController.m:
- (void)eraseMe:(UITextField *)textField
{
textField.text=@"";
}
- (void)singleTap {
singleLabel.text=@"Single Tap Detected";
[self performSelector:@selector{eraseMe:) withObject:singleLabel afterDelay:1.6f];
}
- (void)doubleTap {
//类似于singleTap
}
...
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch=[touches anyObject];
NSUInteger tapCount=[touch tapCount];
switch (tapCount) {
case 1:
[self singleTap];
break;
case 2:
...
}
}
@end
注意,我们没有在该程序中实现touchesEnded和touchesMoved方法。没有通知我们用户已经停止轻击,这给我们出了一道难题。你已经熟悉了performSelector:withObject:afterDelay:,他允许我们在将来的某个时间调用方法。另一个方法允许我们取消这些将来的调用。他是一个名为cancelPreviousPerformReq
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch=[touches anyObject];
NSUInteger tapCount=[touch tapCount];
switch (tapCount) {
case 1:
[self performSelector:@selector(singleTap) withObject:nil afterDelay:.4];
break;
case 2:
[NSObject cancelPreviousPerformReq
[self performSelector:@selector(doubleTap) withObject:nil afterDelay:.4];
case 3:
[NSObject cancelPreviousPerformReq
[self performSelector:@selector(tripleTap) withObject:nil afterDelay:.4];
case 4:
[NSObject cancelPreviousPerformReq
[self performSelector:@selector(quadrupleTap) withObject:nil afterDelay:.4];
default:
break;
}
}
检测捏合操作
另一个常见的手势是两个手指的捏合。检测捏合非常简单。首先,当手势开始时,我们检查以确保存在两个触摸,因为捏合是两个手指的手势。如果有两个触摸,则存储他们之间的距离。然后,随着手势的进行,我们始终检测两个手指之间的距离,如果该距离增大或减小的量大于特定数量,我们便知道存在捏合手势。
在创建一个基于视图的项目PinchMe。在13 PinchMe文件夹中找到两个名为CGPointUtils.h和CGPointUtils.c的文件,并将他们拖到你项目的Classes文件夹中。你可以在自己的应用程序中自由使用这些实用函数。
单击PinchMeViewController.h:
#define kMinimumPinchDelta 100
@interface ... {
IBOutlet UILabel *label;
CGFloat initialDistance;
}
- (void)eraseLabel;
@end
打开PinchMeViewController.xib。设置视图接受多触摸,拖动一个Label到View上。设置输出口。
然后PinchMeViewController.m:
#import "CGPointUtils.h"
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count]==2) {
NSArray *twoTouches=[touches allObjects];
UITouch *first=[twoTouches objectAtIndex:0];
UITouch *second=[twoTouches objectAtIndex:1];
initialDistance=distanceBetweenPoints([first locationInView:self.view],[second locationInView:self.view]);
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count]==2) {
NSArray *twoTouches=[touches allObjects];
UITouch *first=[twoTouches objectAtIndex:0];
UITouch *second=[twoTouches objectAtIndex:1];
CGFloat currentDistance=distanceBetweenPoints([first locationInView:self.view],[second locationInView:self.view]);
if (initialDistance == 0)
initialDistance=currentDistance;
else if (currentDistance - initialDistance >kMinimumPinchDelta ) {
label.text=@"Outward Pinch";
[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6f];
}
else if ( initialDistance -currentDistance >kMinimumPinchDelta ) {
label.text=@"Inward Pinch";
[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6f];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
initialDistance=0;
}
编译和运行以进行尝试。记住,你可以通过按下Option键并在仿真器窗口中使用鼠标单击拖动来模仿捏合手势。
自己定义手势
如果你将手势定义的太严格,那他就没什么用处;定义的太笼统,那么操作就又会太灵活。自己定义手势,你必须确切知道某个手势的不精确之处。如果尝试捕获某个复杂的手势(比如说数字8),那么检测该手势背后的数学也将非常复杂。
在本例中,我们将定义一个形状像选中标记的手势,√ 。
定义此选中标记手势需要哪些属性呢?首要的一点是这两条直线之间的角度的锐角变化。我们还希望确保在形成锐角角度之前,用户的手指在直线上移动了一点距离。选中标记的腿以某个尖锐的角度相交,仅小于90度。需要严格85度角的手势很难做对,因此我们将定义一个可接受角度的范围。
创建一个基于视图的新项目 CheckPlease。同时添加CGPointUtils.h和CGPointUtils.c。
展开Classes文件夹,更改CheckPleaseViewControlle
#define kMinimumCheckMarkAngle 50
#define kMaximumCheckMarkAngle 135
#define kMinimumCheckMarkLength 10
@interface ..
IBOutlet UILabel *label;
CGPoint lastPreviousPoint;
CGPoint lastCurrentPoint;
CGFloat lineLengthSoFar;
}
CheckPleaseViewControlle
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch=[touches anyObject];
CGPoint point=[touch locationInView:self.view];
lastPreviousPoint=point;
lastCurrentPoint=point;
lineLengthSoFar=0.0f;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch=[touches anyObject];
CGPoint previousPoint=[touch previousLocationInView:self.view];
CGPoint currentPoint=[touch locationInView:self.view];
CGFloat angle=angleBetweenLines(lastPreviousPoint,lastCurrentPoint,previousPoint,currentPoint);
if (angle>=kMinimumCheckMarkAngle && angle <=kMaximumCheckMarkAngle && lineLengthSoFar>kMinimumCheckMarkLength) {
label.text=@"Checkmark";
[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6];
}
lineLengthSoFar +=distanceBetweenPoints(previousPoint,currentPoint);
lastPreviousPoint=previousPoint;
lastCurrentPoint=currentPoint;
}
当你为自己的应用程序定义新手势时,确保仔细测试过他们,如果可以的话,还要让其他帮助你测试。一定要确保手势容易操作,且不是无意中就可以触发那么容易。还要确保不与其他应用程序中的其他手势发生冲突。例如,不应该将一个手指的手势算作自定义手势和捏合手势。