iOS开发

UIGestureRecognizer and UIMenuCo

2015-08-08  本文已影响404人  ilaoke

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

上一篇下一篇

猜你喜欢

热点阅读