UIGestureRecognizer and UIMenuController

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》第十三章的总结。

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

推荐阅读更多精彩内容