UIGestureRecognizer and UIMenuController
UIGestureRecognizer有许多子类,响应不同的手势。
为UIGestureRecognizer实例指定target-action,并将UIGestureRecognizer实例绑定到view上,当UIGestureRecognizer实例识别view上的某种手势后,他会发送指定的action消息到target。
所有的UIGestureRecognizer action 消息都是以下的形式:
- (void)action:(UIGestureRecognizer *)gestureRecognizer;
Gesture recognizer拦截view上的touch事件,因此一个有gesture recognizer的view可能不会收到UIResponder的消息,如touchesBegan:withEvent:
UITapGestureRecognizer
UITapGestureRecognizer是UIGestureRecognizer的子类。
在BKDrawView.m,在初始化方法中为其绑定监听双击的gesture recognizer,gesture recognizer的target是view本身,action为doubleTap,所以当双击事件被监听到,会执行target的doubleTap方法。
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.linesInProgress = [[NSMutableDictionary alloc] init];
self.finishedLines = [[NSMutableArray alloc] init];
self.backgroundColor = [UIColor grayColor];
// 启用multiple touches
self.multipleTouchEnabled = YES;
// 创建监听双击的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 将gesture recognizer关联到view上
[self addGestureRecognizer:doubleTapRecognizer];
}
return self;
}
- (void)doubleTap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized Double Tap");
[self.linesInProgress removeAllObjects];
[self.finishedLines removeAllObjects];
[self setNeedsDisplay];
}
doubleTap方法的参数是gesture recognizer,并且是这个gesture recognizer发送了doubleTap消息到target.
此时双击view,会显示如下log:
2015-08-06 00:20:46.639 TouchTracker[3301:70b] touchesBegan:withEvent:
2015-08-06 00:20:46.767 TouchTracker[3301:70b] Recognized Double Tap
2015-08-06 00:20:46.768 TouchTracker[3301:70b] touchesCancelled:withEvent:
Gesture recognizer检查触摸事件,来判断是否其监听的手势发生了。在gesture recognizer识别手势之前,这些UIResponder 消息仍会正常发送给view。
当touch事件发生,而gesture recognizer还不能识别为其监听的手势时,则touchesBegan:withEvent: 会被发送给view,而当gesture recognizer识别了手势,UIResponder消息不再发送给view,为了告诉view touch事件被接管,会发送touchesCancelled:withEvent:给view。
为阻止这种情况发生,可以让gesture recognizer延迟发送touchesBegan:withEvent: 给view,即当touch不再可能被识别成特定的gesture了,再发送touchesBegan:withEvent: 消息给view。
// 当touch不再可能被识别为double tap gesture了,再发送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
Multiple Gesture Recognizer
再为BKDrawView添加监听单击的gesture recognizer:
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.linesInProgress = [[NSMutableDictionary alloc] init];
self.finishedLines = [[NSMutableArray alloc] init];
self.backgroundColor = [UIColor grayColor];
// 启用multiple touches
self.multipleTouchEnabled = YES;
// 创建监听双击的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 当touch不再可能被识别为double tap gesture了,再发送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
// 将gesture recognizer关联到view上
[self addGestureRecognizer:doubleTapRecognizer];
// 添加监听单击的gesture recognizer
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
tapRecognizer.delaysTouchesBegan = YES;
[self addGestureRecognizer:tapRecognizer];
}
return self;
}
- (void)doubleTap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized Double Tap");
[self.linesInProgress removeAllObjects];
[self.finishedLines removeAllObjects];
[self setNeedsDisplay];
}
- (void)tap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized tap");
}
现在view上有两个gesture recognizer,双击view,既会触发监听单击的gesture recognizer,也会触发监听双击的gesture recognizer,此时需要在这些gesture recognizers之间添加dependency,就像在说”你等下,这个gesture可能是我的“。
// 单击gesture recognizer要等待双击gesture recognizer失败再触发
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
UIMenuController
UIMenuController 包含一组UIMenuItem对象,每个menu item有一个title和action(action消息被发送给window的first responder)。
一个application只有一个UIMenuController对象,当要显示menu controller时,要为其设置menu items,指定一个距形去显示,并设置为可见。
- (void)tap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized tap");
// 找到当前手势在view中的坐标
CGPoint point = [gr locationInView:self];
self.selectedLine = [self lineAtPoint:point];
if (self.selectedLine) {
// 将view本身设置为window的first responder
[self becomeFirstResponder];
// 获得menu controller单例
UIMenuController *menu = [UIMenuController sharedMenuController];
// 创建menu item,并指定title和action
UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteLine:)];
// 为menu controller 设置menu items
menu.menuItems = @[deleteItem];
// 为menu controller指定距形
[menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self];
// 将menu controlelr设置为可见
[menu setMenuVisible:YES animated:YES];
}else{
// 隐藏menu controller
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
}
[self setNeedsDisplay];
}
上面代码中,首先将view自身设置为window的first responder,一个自定义view要成为first responder,必须重写canBecomeFirstResponder方法,并返回YES.
// 重写canBecomeFirstResponder方法,并返回YES,使得当前VIEW可以成为first responder
- (BOOL)canBecomeFirstResponder{
return YES;
}
现在运行程序,你会发现menu并没有出现,因为first responder没有menu item对应的action方法,添加deleteLine方法:
- (void)deleteLine:(id)sender{
[self.finishedLines removeObject:self.selectedLine];
// Redraw everything
[self setNeedsDisplay];
}
UILongPressGestureRecognizer
为BKDrawView添加long press gesture recognizer,默认touch持续0.5秒即为long press,可以修改minimumPressDuration来改变持续时间。
// 添加监听长按的gesture recognizer
UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
[self addGestureRecognizer:pressRecognizer];
不像tap这种简单的gesture recognizer,long press gesture recognizer有三种state:UIGestureRecognizerStatePossible, UIGestureRecognizerStateBegan, UIGestureRecognizerStateEnded。
当gesture recognizer状态变成非possible时,就会发送action消息到target,所以在press gesture的开始和结束状态,target都会收到action消息。
下面当长按屏幕时,选中最近的一条线,当长按结束,释放选中的线:
- (void)longPress:(UIGestureRecognizer *)gr{
if(gr.state == UIGestureRecognizerStateBegan){
CGPoint point = [gr locationInView:self];
self.selectedLine = [self lineAtPoint:point];
if (self.selectedLine) {
[self.linesInProgress removeAllObjects];
}
}else if(gr.state == UIGestureRecognizerStateEnded){
self.selectedLine = nil;
}
[self setNeedsDisplay];
}
UIPanGestureRecognizer
当用户长按线条,然后移动线条,这个动作就叫panning。
通常gesture recognizer不分享其捕获的touch,一旦识别为了gesture,这些touch不会再被其他gesture recognizer处理。然而pan gesture发生在long press gesture中,需要这两种gesture recognizer能够同时识别gesture。
为了实现这种共享touch,需要实现UIGestureRecognizerDelegate protocol的
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,当此方法返回YES,gesture recognizer之间可以分享touches。
pan gesture recognizer支持changed state,当手指开始移动,pan recognizer进入began state并发送action消息到target,当手指继续移动,pan gesture recognizer进入changed state并发送action消息到target。最后,当手指离开屏幕,pan gesture recognizer进入ended state并发送最后一次action消息到target。
@interface BKDrawView () <UIGestureRecognizerDelegate>
//@property (nonatomic, strong) BKLine *currentLine;
// 保存当前的多个line
@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
@property (nonatomic, strong) NSMutableArray *finishedLines;
@property (nonatomic, weak) BKLine *selectedLine;
@property (nonatomic, strong) UIPanGestureRecognizer *moveRecognizer;
@end
@implementation BKDrawView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.linesInProgress = [[NSMutableDictionary alloc] init];
self.finishedLines = [[NSMutableArray alloc] init];
self.backgroundColor = [UIColor grayColor];
// 启用multiple touches
self.multipleTouchEnabled = YES;
// 创建监听双击的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 当touch不再可能被识别为double tap gesture了,再发送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
// 将gesture recognizer关联到view上
[self addGestureRecognizer:doubleTapRecognizer];
// 添加监听单击的gesture recognizer
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
tapRecognizer.delaysTouchesBegan = YES;
// 单击gesture recognizer要等待双击gesture recognizer失败再触发
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
// 添加监听长按的geture recognizer
UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
[self addGestureRecognizer:pressRecognizer];
// pan gesture recognizer
self.moveRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveLine:)];
self.moveRecognizer.delegate = self;
self.moveRecognizer.cancelsTouchesInView = NO;
[self addGestureRecognizer:self.moveRecognizer];
}
return self;
}
- (void)moveLine:(UIPanGestureRecognizer *)pgr{
if (!self.selectedLine) {
return;
}
// When the pan recognizer changes its positon
if (pgr.state == UIGestureRecognizerStateChanged) {
// translationInView方法返回,pan gesture已经移动了多远
CGPoint translation = [pgr 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];
// 重置translationInVew为零,使得从上一次change事件后从0开始计算translation
[pgr setTranslation:CGPointZero inView:self];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if (gestureRecognizer == self.moveRecognizer) {
return YES;
}
return NO;
}
UIPanGestureRecognizer的translationInView:方法返回pan gesture移动了多远,返回的坐标CGPoint是在X,Y轴上移动距离的值。
每个UIGestureRecognizer都有cancelsTouchesInView属性,默认值是YES,意思是gesture recognizer会吃掉其识别的touch event,从而不会触发UIResponder的方法,如touchesBegan:withEvent:,将其设置为NO,从而使得touchesMoved:withEvent:可以被执行,因为gesture recognizer是拦截器,他控制了是否执行UIResponder的方法。
UIResponderStandardEditActions
UIResponderStandardEditActions协议声明了UIMenuController中的action方法,如果view实现了这些方法,当UIMenuController显示时,就会显示对应的menu item。
判断view是否实现了某方法,由canPerformAction:withSender:方法来判断,menu controller 会发送该消息给view,默认是当view实现了某方法,就返回YES,否则返回NO,我们可以重写该方法。
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{
//return [super canPerformAction:action withSender:sender];
return YES;
}
除了上面所讲的gesture recognizer,还有三个内置的gesture recognizer: UIPinchGestureRecognizer, UISwipeGestureRecognizer, UIRotationGestureRecognizer。
本文是对《iOS Programming The Big Nerd Ranch Guide 4th Edition》第十三章的总结。