首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》13.6 UIPanGestureRecognizer以及同时识别多个手势

关灯直达底部

当用户按住某根线条不放时,TouchTracker应该允许用户通过移动手指来拖曳选中的线条。这类手势称为拖动(pan),可以用UIPanGestureRecognizer对象来识别。

通常情况下,UIGestureRecognizer对象不会将其处理过的触摸事件再交给其他对象来处理。一旦某个UIGestureRecognizer子类对象识别出了相应的手势,就会“吃掉”所有相关的触摸事件,导致其他UIGestureRecognizer对象没有机会再处理这些触摸事件。对TouchTracker,这种特性会导致BNRDrawView对象无法处理拖动手势,这是因为整个拖动手势都是在长按手势中发生的。要解决这个问题,需要让UILongPressGestureRecognizer对象和UIPanGestureRecognizer对象能够同时识别手势。

在BNRDrawView.m的类扩展中将BNRDrawView声明为遵守UIGestureRecognizer- Delegate协议。然后声明一个类型为UIPanGestureRecognizer的属性,代码如下:

@interface BNRDrawView () <UIGestureRecognizerDelegate>

@property (nonatomic, strong) UIPanGestureRecognizer *moveRecognizer;

@property (nonatomic, strong) NSMutableDictionary *linesInProgress;

@property (nonatomic, strong) NSMutableArray *finishedLines;

@property (nonatomic, weak) BNRLine *selectedLine;

@end

更新BNRDrawView.m中的initWithFrame:,创建一个UIPanGestureRecognizer对象,设置属性,然后将该对象附着在BNRDrawView对象上,代码如下:

[self addGestureRecognizer:pressRecognizer];

self.moveRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self

action:@selector(moveLine:)];

self.moveRecognizer.delegate = self;

self.moveRecognizer.cancelsTouchesInView = NO;

[self addGestureRecognizer:self.moveRecognizer];

UIGestureRecognizerDelegate协议声明了很多方法,目前BNRDrawView只需要用到其中的一个:gestureRecognizer:shouldRecognizeSimultaneouslyWith- GestureRecognizer:。当某个UIGestureRecognizer子类对象识别出特定的手势后,如果发现其他的UIGestureRecognizer子类对象也识别出了特定的手势,就会向其委托对象发送gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:消息。如果相应的方法返回YES,那么当前的UIGestureRecognizer子类对象就会和其他UIGestureRecognizer子类对象共享UITouch对象。

在BNRDrawView.m中,如果消息的发送方是_moveRecognizer,就返回YES;否则返回NO,代码如下:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer

shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)other

{

if (gestureRecognizer == self.moveRecognizer) {

return YES;

}

return NO;

}

完成上述修改后,当用户长按某根线条不放时,UIPanGestureRecognizer对象也能收到相关的UITouch对象,从而可以跟踪用户的手指移动。当用户的手指开始移动时,UIPanGestureRecognizer对象的状态也会切换至“开始”。如果UILongPress- GestureRecognizer对象和UIPanGestureRecognizer对象不能同时识别手势,那么当用户的手指开始在屏幕上移动时,UILongPressGestureRecognizer对象的状态还是会切换至“开始”,但是UIPanGestureRecognizer对象的状态不会发生变化,也不会向其目标对象发送动作消息。

除了之前介绍的三种状态,UIPanGestureRecognizer对象还有一种变化后(changed)状态。当手指开始移动时,UIPanGestureRecognizer对象会进入“开始”状态,并向其目标对象发送指定的动作消息。当手指在屏幕上移动时,UIPanGestureRecognizer对象的状态会切换至“变化后”状态,并持续地向其目标对象发送指定的动作消息。最后,当手指离开屏幕时,UIPanGestureRecognizer对象的状态会切换至“结束”状态,并向其目标对象最后一次发送指定的动作消息。

下面要实现UIPanGestureRecognizer对象的动作方法moveLine:。在moveLine:中,要调用UIPanGestureRecognizer 对象的translationInView:方法。该方法会根据传入的UIView对象的坐标系,以CGPoint结构的形式返回手指的拖动距离。当拖动手势开始时,拖动距离是0点(x和y都是0)。拖动的过程中,拖动距离会不断发生变化(将手指移动至窗口的最右端,x的值就会很高。将手指移回手势的起始位置,拖动距离就会变回0点)。

在BNRDrawView.m中实现moveLine:。因为调用该方法的将是UIPanGestureRecognizer对象,所以可以将moveLine:的传入实参类型声明为UIPanGestureRecognizer对象,代码如下:

- (void)moveLine:(UIPanGestureRecognizer *)gr

{

// 如果没有选中的线条就直接返回

if (!self.selectedLine) {

return;

}

// 如果UIPanGestureRecognizer对象处于“变化后”状态

if (gr.state == UIGestureRecognizerStateChanged) {

// 获取手指的拖移距离

CGPoint translation = [gr translationInView:self];

// 将拖移距离加至选中的线条的起点和终点

CGPoint begin = self.selectedLine.begin;

CGPoint end = self.selectedLine.end;

begin.x += translation.x;

begin.y += translation.y;

end.x += translation.x;

end.y += translation.y;

// 为选中的线条设置新的起点和终点

self.selectedLine.begin = begin;

self.selectedLine.end = end;

// 重画视图

[self setNeedsDisplay];

}

}

构建并运行应用。画一根线条,按住这根线条不放并开始拖移。读者会发现当前选中的线条位置并不能和手指的位置保持一致。这是因为moveLine:会持续累加当前选中的线条的起点和终点。如果UIPanGestureRecognizer对象可以增量地报告拖移距离(以上次调用moveLine:时的位置为起点),就能解决上述问题。UIPanGestureRecognizer有一个名为setTranslation:的方法,调用该方法并传入CGPointZero,就能将手指的当前位置设置为拖移手势的起始位置。因此,只需要在UIPanGestureRecognizer对象报告位置变化时,向其发送setTranslation:消息并传入CGPointZero,就能使该对象增量地报告拖移距离。

在BNRDrawView.m的moveLine:底部,加入下面这行代码。

[self setNeedsDisplay];

[gr setTranslation:CGPointZero inView:self];

}

}

构建并运行应用,画一根线条,按住这根线条不放并开始拖移。当前选中的线条位置应该会和手指的位置保持一致。

在BNRDrawView.m的initWithFrame:中,TouchTracker还设置了UIPanGesture- Recognizer对象的cancelsTouchesInView属性。该属性的默认值是YES,当某个UIGestureRecognizer对象的cancelsTouchesInView属性为YES时,这个对象会在识别出特定的手势时,“吃掉”所有和该手势有关的UITouch对象。这样,该对象所依附的UIView对象将不会收到之前介绍过的那些UIResponder消息,例如touchesBegan:withEvent:。

通常情况下,上述特性是符合开发预期的,但是也有例外。以TouchTracker为例,如果UIPanGestureRecognizer对象在识别出拖移手势时吃掉了所有相关的UITouch对象,那么BNRDrawView对象将没有机会处理这些UITouch对象,也就无法创建相应的BNRLine对象。

当某个UIGestureRecognizer对象的cancelsTouchesInView属性为NO时,这个对象所依附的UIView对象仍然会收到相应的UIResponder消息,从而有机会处理相关的UITouch对象。读者可以尝试注释掉那行设置cancelsTouchesInView属性的代码,检验效果。