Event Handling Guide for iOS(二)

手势识别器

手势识别器将底层的事件处理代码转化为高层次的行为。它们是你可以添加到视图中的对象,让你的视图具备control一样响应操作的能力。手势识别器将对触摸进行分析以匹配特定的手势,例如轻扫、缩放、旋转手势。如果他们识别到指派的手势,它们将发送动作消息给目标对象(target)。目标对象的典型代表是视图控制器,它们对手势的响应可参照图1-1。这种设计模式不仅强大而且简单;你能动态的决定那个视图去响应操作,也不需要因为为视图添加手势识别器而创建视图的子类。

Snip20170907_3.png

通过手势识别器简化事件处理

UIKit框架提供了预先定义的手势识别器来监测一般的手势。最好能够使用预先定义的手势识别器,因为这能大大的减少你的代码量。另外,使用标准的手势识别器而不是自定义的手势识别器,能保证你的APP行为在用户预期内。

如果希望你的APP能识别一种独一无二的手势,例如钩玄、涡旋,那么你可以创建你自定义的手势识别器。学习如何设计和实现自定义的手势识别器,请看"创建自定义的手势识别器"(在后续文章中)。

内置手势识别器中的常用手势

当你设计APP时,你需要考虑使用哪种手势识别器。对比每一种手势,下表中的内置手势识别器是否已经足够:


Snip20170908_4.png

你的APP对于手势的响应必须满足用户的期望。例如,一个捏合手势(pinch)就应该进行缩放,而一个点击手势(tap)就应该是选择什么内容。关于如何恰如其分的使用手势的参考,请查阅iOS Human Interface Guidelines中的“iOS Human Interface Guidelines”。

添加到视图中的手势识别器

每一种手势识别器都和某一个视图相关联。另外,一个视图可以包含多种手势识别器,因为一个单一的视图可以响应许多不同的手势。如果你希望手势识别器能够识别发生在特定视图上的触摸事件,那么你必须将手势识别器添加到这个视图上。当用户触摸这个视图时,手势识别器会在视图之前收到一个触摸的消息。最终,手势识别器将代表视图响应触摸。

手势触发操作消息

当手势识别器识别到指定的手势,便发送给操作消息到它的目标对象。创建一个手势识别器需要初始化它的目标对象和响应方法。

离散的、连续的手势

手势要么是离散的就是连续的。点击就是离散的手势,只发生一次。缩放则是连续的,需要一段时间。对于离散的手势,手势识别器向它的目标对象发送单一的操作消息。而连续的手势,手势识别器将会持续的发送操作消息到目标对象,直到多点触控序列终止。 如图1-2:

Snip20170908_5.png

通过手势识别器响应事件

将内置的手势识别器添加到APP中,你需要做如下三件事:

  • 创建和配置一个手势识别器实例,这个步骤包括制定一个目标对象、响应方法,以及手势的一些特有属性(例如,需要几根指头)。
  • 将手势识别器添加到视图。
  • 实现响应方法以处理手势。

通过界面生成器(XIB)添加手势识别器

Xcode中的界面生成器,对于添加手势识别器和添加其他任意对象到界面中的方式是一样的---从对象库中拖拽一个手势识别器到视图上。这样操作之后,手势是识别器会被自动添加到视图中去。你可以检查手势识别器到底添加到了哪一个视图当中了,如果有必要,你也可以在NIB文件中修改连接

创建手势识别器对象之后,你需要去建立并连接响应方法。这个响应方法将会在手势识别器识别到手势时调用,你也可以创建并连接手势识别器的关联属性。你的代码应该参照清单1-1:

清单1-1 通过XIB添加手势识别器:

@interface APLGestureRecognizerViewController ()
@property (nonatomic, strong) IBOutlet UITapGestureRecognizer *tapRecognizer;
@end
@implementation
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer
     // Will implement method later...
}
@end

通过代码添加手势识别器

你可以在代码中通过配置并初始化一个具体的UIGestureRecognizer子类来创建手势识别器,例如UIPinchGestureRecognizer。你可以参照清单1-2来指定目标对象和响应方法以初始化一个手势识别器,在大多数情况下,目标对象会是视图所在的视图控制器。

清单1-2 通过代码创建一个单击手势识别器:

 - (void)viewDidLoad {
     [super viewDidLoad];

  // Create and initialize a tap gesture
       UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
            initWithTarget:self action:@selector(respondToTapGesture:)];
       // Specify that the gesture must be a single tap
       tapRecognizer.numberOfTapsRequired = 1;
       // Add the tap gesture recognizer to the view
       [self.view addGestureRecognizer:tapRecognizer];
       // Do any additional setup after loading the view, typically from a nib
  }

响应离散手势

当你创建一个手势识别器,你会将它与一个响应方法进行连接。利用这个响应方法来响应手势识别器对应的手势。清单1-3展示了一个响应离散手势的例子。当用户点击了添加了手势识别器的视图,控制器将会显示一个图片以示发生了点击。showGestureForTapRecognizer:方法通过手势识别器的locationInView:方法来确定手势在视图中的位置,并将图片显示在这个位置上。

接下来的三段式里代码来自于Simple Gesture Recognizers示例工程,你可以查看该工程获取更多信息。

清单 1-3 处理一个双击手势

- (IBAction)showGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer {
         // Get the location of the gesture
        CGPoint location = [recognizer locationInView:self.view];
         // Display an image view at that location
        [self drawImageForGestureRecognizer:recognizer atPoint:location];
         // Animate the image view so that it fades out
      [UIView animateWithDuration:0.5 animations:^{
             self.imageView.alpha = 0.0;
      }];
 }

每一种手势识别器都有自己特有的属性列表。例如,在清单1-4中,showGestureForSwipeRecognizer:方法使用了轻扫手势识别器的方法(direction)属性来确定用户是向左还是向右轻扫。然后,利用该属性的值将图片在轻扫的方向上淡出。

清单 1-4 响应向右或向左的轻扫手势

// Respond to a swipe gesture
- (IBAction)showGestureForSwipeRecognizer:(UISwipeGestureRecognizer *)recognizer
{
}
// Get the location of the gesture
CGPoint location = [recognizer locationInView:self.view];
// Display an image view at that location
[self drawImageForGestureRecognizer:recognizer atPoint:location];
// If gesture is a left swipe, specify an end location
// to the left of the current location
if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
     location.x -= 220.0;
} else {
     location.x += 220.0;
}
// Animate the image view in the direction of the swipe as it fades out
[UIView animateWithDuration:0.5 animations:^{
     self.imageView.alpha = 0.0;
     self.imageView.center = location;
}];

响应连续的手势

连续的手势允许你的APP响应正在发生的手势。例如,你的APP能在用户捏合(pinch)的时候不断的进行缩放,也可在用户拖拽的时候围绕着屏幕运动。

清单1-5展示了一个与手势相同角度不断旋转的图片,并且当用户停止转动时,让图片旋转回到水平,与此同时将图片进行动画淡出。当用户旋转指头,showGestureForRotationRecognizer:方法会被持续的调用直到所有的手指离开屏幕。

清单 1-5 响应旋转手势

// Respond to a rotation gesture
- (IBAction)showGestureForRotationRecognizer:(UIRotationGestureRecognizer
*)recognizer {
       // Get the location of the gesture
       CGPoint location = [recognizer locationInView:self.view];
       // Set the rotation angle of the image view to
       // match the rotation of the gesture
       CGAffineTransform transform = CGAffineTransformMakeRotation([recognizer
rotation]);
       self.imageView.transform = transform;
       // Display an image view at that location
       [self drawImageForGestureRecognizer:recognizer atPoint:location];
      // If the gesture has ended or is canceled, begin the animation
      // back to horizontal and fade out
      if (([recognizer state] == UIGestureRecognizerStateEnded) || ([recognizer
state] == UIGestureRecognizerStateCancelled)) {
           [UIView animateWithDuration:0.5 animations:^{
                self.imageView.alpha = 0.0;
                self.imageView.transform = CGAffineTransformIdentity;
            }];
       }
}

每当showGestureForRotationRecognizer: 方法被调用,在drawImageForGestureRecognizer:方法中,图片都会被设置为不透明的。当手势结束,图片在animateWithDuration: 方法中被设置为透明的。showGestureForRotationRecognizer:根据校对手势识别器的状态确定了手势是否已经完成。手势识别器的状态会在稍后的文章中进行详细的说明。

定义手势的交互方式

一般来说,当你添加手势识别器到你的APP,你需要对手势识别器如何进行交互以及触控事件的处理代码有明确的期望。要做到这一点,你首先应该充分理解手势识别器是如何工作的。

手势识别器在有限的状态机下运转

手势识别器按照预先定义的方式从一种状态过渡到另外一种状态。手势识别器会根据遇到的确切条件从一种状态切换到任何一种可能的状态下。精准的状态机变化依赖于手势识别器是离散的还是连续的,参见图1-3。所有的手势识别器处于可能的状态(UIGestureRecognizerStatePossible)。他们分析收到的任何多点触控序列,在分析过程中要么识别失败就识别成功。识别识别以为这手识别器切换到失败状态(UIGestureRecognizerStateFailed)。

图1-3 手势识别器状态机

Snip20170911_6.png

当一个离散的手势识别器识别到手势,他的状态由可能(Possible)变为识别到的(UIGestureRecognizerStateRecognized),并且识别过程结束。

对于连续的手势,手势识别器的状态在第一次识别到手势时由可能变为开始(UIGestureRecognizerStateBegan)。然后变为改变的(UIGestureRecognizerStateChanged),并且将不断的由改变的转换为改变的。当用户最后的手指离开视图,状态变为结束的(UIGestureRecognizerStateEnded)并且手势识别结束。注意,结束状态是识别到的(UIGestureRecognizerStateRecognized)别名而已。

如果连续手势的识别器判定手势不满足期望的模式,识别器也能从改变的状态变化为取消状态(UIGestureRecognizerStateCancelled)。

每当手势识别器的状态改变,他都将发送动作消息到他的目标对象,除非状态改变为失败或者取消。因此一个离散的手势识别器只发送单个动作消息,当状态由可能变为识别到的。而一个连续的手势识别器在状态改变时会发送很多动作消息。

当手势识别器的状态变为识别到的(结束的)状态,便重置状态到可能的(Possible),此过程不发送动作消息。

与其他手势识别器交互

一个视图可以添加多个手势识别器。使用视图的gestureRecognizers属性来决定哪个手势被添加到视图中去。你可以动态的改变一个视图如何处理手势,分别通过addGestureRecognizer:removeGestureRecognizer:方法来添加和移除手势。

当一个视图添加了多点触控手势识别器,你可能希望改变手识别器对于触摸事件接收和分析的竞争方式。在默认情况下,并没有设定的顺序规定哪个手势识别器最先收到触摸事件,因为这个原因,触摸事件每次会以不同的顺序发送给手势识别器。你可以覆写这一默认的行为:

  • 指定一个手势识别器最先收到和分析触摸事件。
  • 允许两个手势同时运作。
  • 阻止一个手势识别器进行触摸事件的接收和分析。

子类通过实现UIGestureRecognizer的类方法、代理方法,和覆写父类方法来实现这些行为。

声明两个手势识别器的特定顺序

想象你希望识别轻扫(Swipe)和拖动(Pan)手势,并且你希望他们触发不同的操作。在默认情况下,当用户试图进行轻扫,这个手势会被解释为拖动(Pan)。因为轻扫手势在满足被解释为轻扫手势(离散的手势)的条件之前满足了被解释为拖动手势(连续的手势)的条件。

如果视图同时能够识别轻扫和拖动,而你希望轻扫手势识别器在拖动手势识别器之前分析触摸事件。如果轻扫手势识别器判定触摸是轻扫,那么拖动手势识别器永远不需要去分析触摸, 如果轻扫手势识别器判定触摸不是轻扫,他会变为失败状态,并且拖动手势识别器应该开始分析触摸事件。

你通过将想要延迟分析事件的手势调用requireGestureRecognizerToFail:方法来声明两个手势识别器的关系,参见清单1-6。在这份清单中,两个手势识别器被添加到同一个视图。

清单1-6 拖动手势识别器需要轻扫手势识别器识别失败

- (void)viewDidLoad {
       [super viewDidLoad];

      // Do any additional setup after loading the view, typically from a nib
      [self.panRecognizer requireGestureRecognizerToFail:self.swipeRecognizer];
}

requireGestureRecognizerToFail:方法向消息接收者(理解为该方法的调用者)发送消息规定只有当其他的手势识别器识别失败后接收者才能开始识别和分析触摸事件。在等待其他手势识别器状态变为失败的过程当中,接收者的状态始终为可能。如果其他的手势识别器识别失败,接收者的状态会发生改变。另一方面,如果其他的手势识别器状态变为识别到的或者开始,接收者手势识别器状态将变为失败状态。更多关于状态变换的信息,请看"手势识别器在有限的状态机下运转"。

注意:如果你的APP同时能够识别单击和双击手势并且单击手势识别器不需要双击手势识别器的失败,此外你希望在双击操作之前收到单击操作,尽管用户进行了双击。 这种行为是有意而为的,因为通常最好的用户体验要能支持多种类型的操作。
如果你希望这两种操作是互斥的,你的单击手势识别器应该需要双击手势识别器的失败。但是,单击响应的操作会稍微滞后于用户的输入,因为单击手势识别器被延迟到双击手势识别器失败之后。

阻止手势识别器分析触摸事件

你可以通过为手势识别器添加代理对象来改变它的行为。UIGestureRecognizerDelegate协议提供了两个方法来供你阻止手势识别器分析触摸事件。你可以使用gestureRecognizer:shouldReceiveTouch:方法或者gestureRecognizerShouldBegin:方法---都是UIGestureRecognizerDelegate协议的可选实现方法。

当触摸开始,如果你能够立刻判定你的手势识别器是否应该考虑这次触摸,那么你可以使用gestureRecognizer:shouldReceiveTouch:方法。这个方法会在每次发生触摸的时候被调用。一个触摸发生时,返回NO的手势识别器不会收到事件的通知。

清单1-7 通过gestureRecognizer:shouldReceiveTouch:代理方法阻止一个自定义视图的单击手势识别器收到触摸事件。 当触摸发生,gestureRecognizer:shouldReceiveTouch:方法别调用。它判断用户是否触摸了自定义视图,如果是的,则阻止单击手势识别器收到触摸事件。

清单1-7 阻止手势识别器收到触摸事件

- (void)viewDidLoad {
    [super viewDidLoad];
    // Add the delegate to the tap gesture recognizer
    self.tapGestureRecognizer.delegate = self;
}


// Implement the UIGestureRecognizerDelegate method
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch {
    // Determine if the touch is inside the custom subview
    if ([touch view] == self.customSubview)){
        // If it is, prevent all of the delegate's gesture recognizers
        // from receiving the touch
        return NO;
}
return YES; }

如果你需要等待足够长的时间才能决定一个手势识别器是否应该分析一个触摸,那么你可以使用* gestureRecognizerShouldBegin:* 代理方法。一般情况下,如果你在UIView或者UIControl的子类中自定义事件处理并且存在手势识别器竞争,那么你可以使用该方法。返回NO将导致手势识别器立马识别失败,允许其他手势识别器继续进行触摸处理。如果手势识别要阻止视图或者控件收到触摸事件,这个方法会在手势识别器试图离开可能(Possible)状态时被调用。

当你的视图或者视图控制器不能作为手势识别器的代理时,你也能直接使用 (gestureRecognizerShouldBegin:UIView)方法。该方法的签名和实现一样。

允许同时进行手势识别

默认情况下,两个手势识别器不能同时识别他们各自的手势。但是假设你希望用户能够同时对视图进行缩放和旋转。你需要通过实现gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法来改变原本的默认行为,该方法为UIGestureRecognizerDelegateprotocol协议的可选实现方法。当一个手势识别器器分析一个手势而且会阻止另一个手势识别器识别该手势时,此方法会被调用,反之亦然。此方法默认返回NO。当你希望两个手势识别器同时进行手势分析时返回YES。

注意:只有在任何一个手势识别器允许同时进行手势识别的情况下,你需要实现这个代理方法并返回YES。然而,这也意味着返回NO不一定能够阻止同时识别手势,因为其他手势识别器的代理可能会返回YES。

指定两个手势识别器的单向关系

如果你想控制两个手势识别器之间是如何交互的,你需要指定一个单向关系,你可以在子类中覆写* canPreventGestureRecognizer:或者canBePreventedByGestureRecognizer:*方法来返回NO(默认返回YES)。例如,如果你希望旋转手势阻止捏合手势,而不希望捏合手势阻止旋转手势,你可以这样指定单向关系:

/// 我的备注,前面说到可以允许两个手势同时进行识别,而一般来说两个手势的识别顺序并不能确定,这里就可以满足进行旋转识别时不进行捏合识别,而进行捏合识别时也会进行旋转识别。
 [rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];

并且覆写旋转手势识别器子类方法来返回NO。关于如何实现UIGestureRecognizer子类,参见"创建自定义手势识别器"(后序文章)。

如果两个手势并不存在互相的阻止,使用gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,参见"允许同时进行手势识别"。默认情况下,一个捏合手势阻止一个旋转手势,反之亦然,因为两个手势不能被同时识别。

与用户界面控件交互

iOS6.0及其以后,默认的控件操作会防止重叠的手势识别器行为。例如,按钮的默认操作是单击。如果你向按钮的父视图添加了单击手势识别器,并点击了按钮,按钮的响应方法会收到触摸事件而不是手势识别器。这种情况出现在手势识别器与默认的控件操作重叠时,例如:

  • 单个手指点击一个UIButton, UISwitch, UIStepper, UISegmentedControl, 和UIPageControl。
  • 单个手指在与UISlider平行的方向上轻扫了UISlider的按钮。
  • 单个手指在与UISwitch平行的方向上拖动了UISwitch的按钮。

如果你自定义了任何一种的控件的子类并希望改变这种默认的行为,直接向控件添加手势识别器而不是它的父视图。然后,这个手势识别器会率先收到触摸事件。一如既往的,请确保你阅读了 iOS Human Interface Guidelines以保证你的APP提供直观的用户体验,尤其是在覆写标准控件的默认行为时。

手势识别器解释原生的触摸事件

到目前为止,你已经了解了手势以及你的APP如何去识别和响应他们。 然而,如何去自定义手势识别器或者如何让手势识别器与视图的触摸事件处理交互,你需要在触摸和事件上面思考得更多。

一个事件包含了当前多点触控序列的所有触摸

在iOS中,一个触摸代表了一个手指在屏幕上的移动。一个手势有一个或者多个触摸,触摸用UITouch对象表示。例如,一个捏缩手势有两个触摸---有两个手指在屏幕上从不同的方向朝各自移动。

一个事件(Event)包含了多点触控序列过程中所有的触摸。一个多点触控序列在一个手指触摸到屏幕时开始,在最后一个手指离开屏幕时结束。当一个手指移动时,iOS发送UITouch对象给事件。一个多点触控事件表示了一个UIEventTypeTouches类型的UIEvent对象。

每一个触摸对象仅仅追踪一个手指,并与多点触控序列的生命周期相同。在整个序列过程中,UIKit追踪手指并更新触摸对象的属性。这些属性包括阶段、在视图中的位置、上一个位置和时间戳。

触摸的阶段表明了触摸何时开始,在移动还是静止,以及何时结束(意味着手指不再触摸屏幕)。如图1-4的描述,一个APP会在触摸的不同阶段受到事件(Event)对象。

图1-4 多点触控序列和触摸阶段

Snip20170912_10.png

注意:一个手指并没有鼠标精确。当用户触摸屏幕,接触的区域其实是椭圆形的而且比我们想象中的位置偏低。这个接触区域的变化取决于手指的尺寸、方向、压力、哪一根手指等因素。底层的多点触控系统帮你分析和计算了触摸点,因此你不需要自己编写代码来实现这些。

APP在触摸处理方法中接收触摸

在多点触控序列过程中,如果在特定的触摸阶段产生了新的触摸或者触摸发生了改变,APP将会发送触摸消息。会调用如下方法:

上面的每一个方法都与一个触摸阶段相关联;例如,touchesBegan:withEvent:方法和UITouchPhaseBegan相关联。触摸阶段保存在触摸对象的phase属性当中。

注意:这些方法并不和手势识别器的状态相关联,例如UIGestureRecognizerStateBeganUIGestureRecognizerStateEnded。手势识别器的状态充分的表明了其本身的阶段,而不是识别到的触摸对象的阶段。

调节发送到视图的触摸

或许在某些情况下,你希望一个视图在手势识别器之前收到触摸。但是,在你能够改变触摸传递到视图的路径之前,你需要理解默认的行为。在这个简单的例子中,当一个触摸发生,触摸对象从UIApplication对象传递到UIWindow对象(我的备注:实际上传递的是UIEvent对象)。然后,窗口(window)对象首先将触摸发送到被添加到触摸点发生处的视图(或者它的父视图)上的手势识别器,在发送给视图本身之前。

图1-5 默认的触摸事件传递步骤

Snip20170912_11.png

手势识别器首先识别触摸

一个窗口(window)对象延迟发送触摸对象给视图,为了手势识别器能首先分析触摸。在延迟过程中,如果手势识别器识别到了触摸手势,那么窗口(window)对象永远不会将触摸对象发送给视图,并且会取消任何之前发送给视图的触摸对象---识别的序列的一部分。

例如,你有一个需要两个手指触摸的离散手势识别器,触摸会被转换成两个不同的触摸对象。当触摸发生,触摸点视图的触摸对象从APP对象发送到window对象,并且接下来发生的序列,如图1-6描述:

图1-6 触摸对象的消息序列

Snip20170912_12.png
  1. 窗口(window)对象在开始阶段发送两个触摸对象---通过touchesBegan:withEvent:方法发送给手势识别器。 手势识别器还没有完成手势的识别,因此它的状态仍是可能(Possible)。窗口对象会将同样的触摸对象发送给手势识别器相关联的视图。
  2. 窗口(window)对象在移动阶段发送两个触摸对象---通过touchesMoved:withEvent:方法发送给手势识别器。 手势识别器还没有完成手势的识别,因此它的状态仍是可能(Possible)。窗口对象会将同样的触摸对象发送给手势识别器相关联的视图。
  3. 窗口(window)对象在结束阶段发送一个触摸对象---通过touchesEnded:withEvent:方法发送给手势识别器。 这个触摸对象没有包含足够的手势信息,但是窗口(window)对象不发送该对象到视图。
  4. 窗口(window)对象在结束阶段发送另一个触摸对象。此时手势识别器识别到了手势,因此其状态变为识别到的。在第一个操作消息发送前,视图调用touchesCancelled:withEvent:方法去取消之前在开始和移动阶段发送的触摸对象。触摸在结束阶段被取消掉(注意:touchesEnded:withEvent:并不会被调用)。

现在假设手势识别器在最后一步判定多点触控序列的分析结果并非自己的手势。它将状态设置为UIGestureRecognizerStateFailed。然后窗口对象通过* touchesEnded:withEvent:*消息体发送两个触摸对象给关联的视图。

一个连续的手势识别器遵循相似的序列,除非它可能在结束阶段前识别到手势。快要识别到手势前,他的状态变为UIGestureRecognizerStateBegan(而不是识别到的)。窗口对象(window)发送随后的多点触控序列中的触摸对象给手势识别器而不是关联的视图。

改变触摸到视图的发送

你可通过修改某些UIGestureRecognizer的属性来改变默认的传递路径。如果你改变了这些属性值,你会得到如下不同的行为:

  • delaysTouchesBegan(默认为NO)---正常情况下,窗口对象会发送开始和移动阶段的触摸对象给视图和手势识别器。设置delaysTouchesBegan为YES阻止窗口发送开始和移动阶段的触摸对象给视图。这样能够保证当一个手势识别器识别手势时,不会有任何触摸对象发送给关联的视图。慎重使用该属性,因为它会导致你的用户界面看起来反应迟钝。

  • delaysTouchesEnded(默认为YES)---当这个属性被设置为YES,它确保视图不会完成一个手势想要取消的动作。当一个手势识别器正在识别触摸事件,窗口对象不会将结束阶段的触摸对象发送给给关联的视图。如果一个手势识别器识别到了手势,这个触摸对象将会被取消。如果手势识别器没有识别到手势,窗口对象会通过delaysTouchesEnded消息发送触摸对象到关联的视图。设置该属性值为NO以允许视图和手势识别器同时分析结束阶段的触摸对象。

    考虑到一种情况,即一个视图有一个需要两个手指 点击的手势识别器,并且用户双击了视图。该属性值被设置为YES,视图会收到如下消息touchesBegan:withEvent:, touchesBegan:withEvent:, touchesCancelled:withEvent:, 和touchesCancelled:withEvent:。如果该属性值被设置为NO,视图会收到如下序列消息:touchesBegan:withEvent:, touchesEnded:withEvent:, touchesBegan:withEvent:, 和 touchesCancelled:withEvent:,这意味着在方法touchesBegan:withEvent:中,视图能够识别双击。(我的备注:我自己的测试和上述效果并不一致)

一个手势识别器监测一个触摸并判断他是否为手势的一部分,他能直接将触摸传递给视图。要做到这样,手势识别器自己调用ignoreTouch:forEvent:,传递触摸对象。

PS:这一章没怎么看明白。--

后序研究

cancelsTouchesInView属性默认值为YES,当手势识别器识别到手势时会调用。
touchesCancelled:withEvent:方法,而touchesEnded:withEvent:不会调用,只有当识别失败才会调用。
如果cancelsTouchesInView被设置为NO,设置delaysTouchesEnded并无作用。

创建自定义手势识别器

为了实现自定义的手势识别器,首先需要创建一个UIGestureRecognizer的子类。然后在子类的头文件中添加如下导入命令:

 #import <UIKit/UIGestureRecognizerSubclass.h>

接下来,从UIGestureRecognizerSubclass.h头文件中拷贝如下方法声明到你的头文件中,这些是你需要在子类中实现的方法:

- (void)reset;
- (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;

这些方法和早前在"APP在触摸处理方法中接收触摸"(在前面内容中)中描述的方法具备相同的方法签名和行为。所有你在子类中覆写的方法都必须调用父类实现,尽管父类只有方法的空实现。

注意UIGestureRecognizerSubclass.h中的state属性现在是可读可写的而不仅仅只是可读,你在子类中使用UIGestureRecognizerState枚举来设置state属性值。

实现自定义手势识别器的触摸事件处理方法

实现自定义手势识别器的四个核心方法为:touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, 和touchesCancelled:withEvent:。利用这些方法,你可以通过设置手势识别器的状态(state)将底层的事件处理转换为高层的手势识别。清单1-8 创建了一个手别识别器,具备离散的单击勾选手势。它记录了手势的中点---上行运动开始的地方---而客户端可以获取这个值。

这个例子只有一个视图,但是大多数APP拥有许多视图。一般情况下,你需要将触摸的位置转换为相对于屏幕的坐标,你才能正确的识别拖动视图的手势。

清单1-8 实现勾选手势识别器

#import <UIKit/UIGestureRecognizerSubclass.h>
 
// Implemented in your custom subclass
// Implemented in your custom subclass
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if ([touches count] != 1) {
        self.state = UIGestureRecognizerStateFailed;
        return; }
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
   
//    CGPoint  nowPoint = [touches.anyObject locationInView:self.view];
//    
//    CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view];
    
    /// 另外一种写法
    CGPoint  nowPoint = [touches.anyObject locationInView:self.view.window];
    
    CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view.window];
    
    // strokeUp is a property
    if (!self.strokeUp) {
        // On downstroke, both x and y increase in positive direction
        if (nowPoint.x >= prevPoint.x && nowPoint.y >= prevPoint.y) {
            self.midPoint = nowPoint;
            // Upstroke has increasing x value but decreasing y value
        } else if (nowPoint.x >= prevPoint.x && nowPoint.y <= prevPoint.y) {
            self.strokeUp = YES;
        } else {
            self.state = UIGestureRecognizerStateFailed;
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    if ((self.state == UIGestureRecognizerStatePossible) && self.strokeUp) {
        self.state = UIGestureRecognizerStateRecognized;
        
        NSLog(@"识别到手势");
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
    self.state = UIGestureRecognizerStateFailed;
}

离散手势和连续手势的状态切换时不同的,在前面的内容中"手势识别器在有限状态机下运转"有描述。当你创建一个自定义的手势识别器,你通过设置相关的状态来声明它是离散的还是连续的。作为示例,清单1-8的勾选手势识别器没有将状态设置为开始的(Begin)或者改变的(Changed),因为它是离散的。

当你实现一个子类的手势识别器是,最重要的事情是你需要正确的设置手势识别器的状态(state)。iOS系统需要知道手势识别器的状态以让他按照预期那样交互。例如,如果你需要同时进行手势识别或者需要某个手势识别器失败,iOS需要明白你当前手势的状态。

关于更多自定义手势识别器,请参见WWDC 2012: Building Advanced Gesture Recognizers

重置手势识别器的状态

如果你的手势识别器变为识别到的、结束的、失败的、或者取消的,UIGestureRecognizer会在返回到可能(Possible)状态前调用reset方法。

清单1-9,实现了reset方法来重置手势识别器内部的各种状态以准备好识别新的手势。手势识别器返回了这个方法后,他将不会收到进程中触摸对象进一步更新。

清单1-9 重置手势识别器

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

推荐阅读更多精彩内容

  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 56,585评论 51 597
  • 手势识别器是附加到视图的对象,将低级别事件处理代码转换为更高级别的操作,它允许视图以控件执行的方式响应操作。 手势...
    坤坤同学阅读 4,051评论 0 9
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,987评论 4 26
  • 在开发过程中,大家或多或少的都会碰到令人头疼的手势冲突问题,正好前两天碰到一个类似的bug,于是借着这个机会了解了...
    闫仕伟阅读 5,294评论 2 23
  • -- iOS事件全面解析 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实...
    翘楚iOS9阅读 2,938评论 0 13