UIGestureRecognizer - BNR
继续上篇UITouch - BNR。该篇将实现线条选择、移动和删除操作。
UIGestureRecognizer有一系列子类,每一个子类都用于识别特定的手势。当识别出一个手势时,手势识别器会拦截视图的触摸事件。
使用UITapGestureRecognizer类,实现当用户连续点击屏幕两次时,全部线条都被清空。
修改BNRDrawView类的initWithFrame:方法如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.linesInProgress = [[NSMutableDictionary alloc] init]; 5 self.finishedLines = [[NSMutableArray alloc] init]; 6 self.backgroundColor = [UIColor grayColor]; 7 self.multipleTouchEnabled = YES; 8 9 UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] 10 initWithTarget:self 11 action:@selector(doubleTap:)]; 12 doubleTapRecognizer.numberOfTapsRequired = 2; 13 doubleTapRecognizer.delaysTouchesBegan = YES; //使第一次点击不产生红点14 [self addGestureRecognizer:doubleTapRecognizer]; 15 } 16 return self; 17 }
实现连续两次点击的doubleTap:响应事件,如下:
1 - (void)doubleTap:(UIGestureRecognizer *)gr { 2 NSLog(@"Recognized Double Tap"); 3 [self.linesInProgress removeAllObjects]; 4 [self.finishedLines removeAllObjects]; 5 [self setNeedsDisplay]; 6 }
添加手势识别,允许用户删除一条选定的线条。修改BNRDrawView类的initWithFrame:方法如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.linesInProgress = [[NSMutableDictionary alloc] init]; 5 self.finishedLines = [[NSMutableArray alloc] init]; 6 self.backgroundColor = [UIColor grayColor]; 7 self.multipleTouchEnabled = YES; 8 9 UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] 10 initWithTarget:self 11 action:@selector(doubleTap:)]; 12 doubleTapRecognizer.numberOfTapsRequired = 2; 13 doubleTapRecognizer.delaysTouchesBegan = YES; 14 [self addGestureRecognizer:doubleTapRecognizer]; 15 16 UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] 17 initWithTarget:self 18 action:@selector(tap:)]; 19 tapRecognizer.delaysTouchesBegan = YES; 20 [tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer]; //区别点击与双击手势 21 [self addGestureRecognizer:tapRecognizer]; 22 } 23 return self; 24 }
实现单击的tap:响应事件,如下:
1 - (void)tap:(UIGestureRecognizer *)gr { 2 NSLog(@"Recognized Tap"); 3 }
在BNRDrawView.m文件的类扩展中声明selectedLine属性,用来表示被用户选中的线条,如下:
@property (nonatomic, weak) BNRLine *selectedLine;
其中,finishedLines数组拥有对selectedLine的强引用。当selectedLine从finishedLines中被删除时,将变为nil。
修改BNRDrawView类中drawRect:方法,使selectedLine显示绿色,如下:
1 - (void)drawRect:(CGRect)rect { 2 [[UIColor blackColor] set]; 3 for (BNRLine *line in self.finishedLines) { 4 [self strokeLike:line]; 5 } 6 [[UIColor redColor] set]; 7 for (NSValue *key in self.linesInProgress) { 8 [self strokeLike:self.linesInProgress[key]]; 9 } 10 if(self.selectedLine) { 11 [[UIColor greenColor] set]; 12 [self strokeLike:self.selectedLine]; 13 } 14 }
获取接近点击处的线条:
1 - (BNRLine *)lineAtPoint:(CGPoint)p { 2 for(BNRLine *l in self.finishedLines) { 3 CGPoint start = l.begin; 4 CGPoint end = l.end; 5 for (float t = 0.0; t <= 1.0; t += 0.05) { 6 float x = start.x + t * (end.x - start.x); 7 float y = start.y + t * (end.y - start.y); 8 if (hypot(x - p.x, y - p.y) < 20.0) { 9 return l; 10 } 11 } 12 } 13 return nil; 14 }
最后修改点击手势响应事件tap:方法如下:
1 - (void)tap:(UIGestureRecognizer *)gr { 2 NSLog(@"Recognized Tap"); 3 4 CGPoint point = [gr locationInView:self]; 5 self.selectedLine = [self lineAtPoint:point]; 6 [self setNeedsDisplay]; 7 }
接下来实现,当用户已经选中一条线条之后,将显示一个菜单提供对该线条的删除操作。
修改tap:方法如下:
1 - (void)tap:(UIGestureRecognizer *)gr { 2 NSLog(@"Recognized Tap"); 3 CGPoint point = [gr locationInView:self]; 4 self.selectedLine = [self lineAtPoint:point]; 5 6 if(self.selectedLine) { 7 //使该视图成为menu item响应方法的目标 8 [self becomeFirstResponder]; 9 UIMenuController *menu = [UIMenuController sharedMenuController]; 10 //创建一个新的删除UIMenuItem 11 UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteLine:)]; 12 menu.menuItems = @[deleteItem]; 13 //设置menu的显示位置 14 [menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self]; 15 [menu setMenuVisible:YES animated:YES]; 16 } else { 17 [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES]; 18 } 19 20 [self setNeedsDisplay]; 21 }
当自定义的视图类需要成为第一响应者时,必须重载canBecomeFirstResponder方法,在BNRDrawView.m中添加如下方法:
1 - (BOOL)canBecomeFirstResponder { 2 return YES; 3 }
如果menu items的响应方法没有实现,该menu就不会显示。实现deleteLine:方法如下:
1 - (void)deleteLine:(id)sender { 2 [self.finishedLines removeObject:self.selectedLine]; 3 [self setNeedsDisplay]; 4 }
运行程序,效果如下:
接下来,实现如下功能,当用户长按住一条线时,该线将被选中,用户能够用手指拖动该线条。修改initWithFrame:方法如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.linesInProgress = [[NSMutableDictionary alloc] init]; 5 self.finishedLines = [[NSMutableArray alloc] init]; 6 self.backgroundColor = [UIColor grayColor]; 7 self.multipleTouchEnabled = YES; 8 9 UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] 10 initWithTarget:self 11 action:@selector(doubleTap:)]; 12 doubleTapRecognizer.numberOfTapsRequired = 2; 13 doubleTapRecognizer.delaysTouchesBegan = YES; 14 [self addGestureRecognizer:doubleTapRecognizer]; 15 16 UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] 17 initWithTarget:self 18 action:@selector(tap:)]; 19 tapRecognizer.delaysTouchesBegan = YES; 20 [tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer]; //区别点击与双击手势 21 [self addGestureRecognizer:tapRecognizer]; 22 23 UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] 24 initWithTarget:self 25 action:@selector(longPress:)]; 26 [self addGestureRecognizer:pressRecognizer]; 27 } 28 return self; 29 }
手势识别器处理长按时,其state属性会经历三种变化,其为 UIGestureRecognizerStatePossible UIGestureRecognizerStateBegan UIGestureRecognizerStateEnded 。
实现长按的响应方法longPress:如下:
1 - (void)longPress:(UIGestureRecognizer *)gr { 2 if (gr.state == UIGestureRecognizerStateBegan) { 3 CGPoint point = [gr locationInView:self]; 4 self.selectedLine = [self lineAtPoint:point]; 5 if (self.selectedLine) { 6 [self.linesInProgress removeAllObjects]; 7 } 8 } else if (gr.state == UIGestureRecognizerStateEnded) { 9 self.selectedLine = nil; 10 } 11 [self setNeedsDisplay]; 12 }
让BNRDrawView遵守UIGestureRecognizerDelegate协议,并添加一个UIPanGestureRecognizer属性,如下:
1 @interface BNRDrawView () <UIGestureRecognizerDelegate> 2 3 @property (nonatomic, strong) UIPanGestureRecognizer *moveRecognizer; 4 @property (nonatomic, strong) NSMutableDictionary *linesInProgress; 5 @property (nonatomic, strong) NSMutableArray *finishedLines; 6 @property (nonatomic, weak) BNRLine *selectedLine; 7 8 @end
修改initWithFrame:方法。此处cancelsTouchesInView属性默认为YES,意味着手势识别器将吞没任何它识别出的手势,这样的话,视图将没有机会对UIRespnder方法做出响应。将其设置为NO,这样,手势识别器能识别的触摸事件也会被UIResponder识别。代码如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.linesInProgress = [[NSMutableDictionary alloc] init]; 5 self.finishedLines = [[NSMutableArray alloc] init]; 6 self.backgroundColor = [UIColor grayColor]; 7 self.multipleTouchEnabled = YES; 8 9 UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] 10 initWithTarget:self 11 action:@selector(doubleTap:)]; 12 doubleTapRecognizer.numberOfTapsRequired = 2; 13 doubleTapRecognizer.delaysTouchesBegan = YES; 14 [self addGestureRecognizer:doubleTapRecognizer]; 15 16 UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] 17 initWithTarget:self 18 action:@selector(tap:)]; 19 tapRecognizer.delaysTouchesBegan = YES; 20 [tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer]; //区别点击与双击手势 21 [self addGestureRecognizer:tapRecognizer]; 22 23 UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] 24 initWithTarget:self 25 action:@selector(longPress:)]; 26 [self addGestureRecognizer:pressRecognizer]; 27 28 self.moveRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveLine:)]; 29 self.moveRecognizer.delegate = self; 30 self.moveRecognizer.cancelsTouchesInView = NO; 31 [self addGestureRecognizer:self.moveRecognizer]; 32 } 33 return self; 34 }
实现UIGestureRecognizerDelegate协议的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,如果该方法返回YES,手势识别器将与其它的识别器分享该触摸事件。如下:
1 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 2 if (gestureRecognizer == self.moveRecognizer) { 3 return YES; 4 } 5 return NO; 6 }
实现移动手势的响应事件moveLine:,如下:
1 - (void)moveLine:(UIPanGestureRecognizer *)gr { 2 if (!self.selectedLine) { 3 return; 4 } 5 if (gr.state == UIGestureRecognizerStateChanged) { 6 CGPoint translation = [gr translationInView:self]; 7 CGPoint begin = self.selectedLine.begin; 8 CGPoint end = self.selectedLine.end; 9 begin.x += translation.x; 10 begin.y += translation.y; 11 end.x += translation.x; 12 end.y += translation.y; 13 self.selectedLine.begin = begin; 14 self.selectedLine.end = end; 15 [self setNeedsDisplay]; 16 [gr setTranslation:CGPointZero inView:self]; 17 } 18 }