UIGestureRecognizer and UIMenuController
UIGestureRecognizer有許多子類,響應(yīng)不同的手勢。
為UIGestureRecognizer實例指定target-action,并將UIGestureRecognizer實例綁定到view上,當(dāng)UIGestureRecognizer實例識別view上的某種手勢后,他會發(fā)送指定的action消息到target。
所有的UIGestureRecognizer action 消息都是以下的形式:
- (void)action:(UIGestureRecognizer *)gestureRecognizer;
Gesture recognizer攔截view上的touch事件,因此一個有g(shù)esture recognizer的view可能不會收到UIResponder的消息,如touchesBegan:withEvent:
UITapGestureRecognizer
UITapGestureRecognizer是UIGestureRecognizer的子類。
在BKDrawView.m,在初始化方法中為其綁定監(jiān)聽雙擊的gesture recognizer,gesture recognizer的target是view本身,action為doubleTap,所以當(dāng)雙擊事件被監(jiān)聽到,會執(zhí)行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;
// 創(chuàng)建監(jiān)聽雙擊的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 將gesture recognizer關(guān)聯(lián)到view上
[self addGestureRecognizer:doubleTapRecognizer];
}
return self;
}
- (void)doubleTap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized Double Tap");
[self.linesInProgress removeAllObjects];
[self.finishedLines removeAllObjects];
[self setNeedsDisplay];
}
doubleTap方法的參數(shù)是gesture recognizer,并且是這個gesture recognizer發(fā)送了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檢查觸摸事件,來判斷是否其監(jiān)聽的手勢發(fā)生了。在gesture recognizer識別手勢之前,這些UIResponder 消息仍會正常發(fā)送給view。
當(dāng)touch事件發(fā)生,而gesture recognizer還不能識別為其監(jiān)聽的手勢時,則touchesBegan:withEvent: 會被發(fā)送給view,而當(dāng)gesture recognizer識別了手勢,UIResponder消息不再發(fā)送給view,為了告訴view touch事件被接管,會發(fā)送touchesCancelled:withEvent:給view。
為阻止這種情況發(fā)生,可以讓gesture recognizer延遲發(fā)送touchesBegan:withEvent: 給view,即當(dāng)touch不再可能被識別成特定的gesture了,再發(fā)送touchesBegan:withEvent: 消息給view。
// 當(dāng)touch不再可能被識別為double tap gesture了,再發(fā)送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
Multiple Gesture Recognizer
再為BKDrawView添加監(jiān)聽單擊的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;
// 創(chuàng)建監(jiān)聽雙擊的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 當(dāng)touch不再可能被識別為double tap gesture了,再發(fā)送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
// 將gesture recognizer關(guān)聯(lián)到view上
[self addGestureRecognizer:doubleTapRecognizer];
// 添加監(jiān)聽單擊的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");
}
現(xiàn)在view上有兩個gesture recognizer,雙擊view,既會觸發(fā)監(jiān)聽單擊的gesture recognizer,也會觸發(fā)監(jiān)聽雙擊的gesture recognizer,此時需要在這些gesture recognizers之間添加dependency,就像在說”你等下,這個gesture可能是我的“。
// 單擊gesture recognizer要等待雙擊gesture recognizer失敗再觸發(fā)
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
UIMenuController
UIMenuController 包含一組UIMenuItem對象,每個menu item有一個title和action(action消息被發(fā)送給window的first responder)。
一個application只有一個UIMenuController對象,當(dāng)要顯示menu controller時,要為其設(shè)置menu items,指定一個距形去顯示,并設(shè)置為可見。
- (void)tap:(UIGestureRecognizer *)gr{
NSLog(@"Recognized tap");
// 找到當(dāng)前手勢在view中的坐標(biāo)
CGPoint point = [gr locationInView:self];
self.selectedLine = [self lineAtPoint:point];
if (self.selectedLine) {
// 將view本身設(shè)置為window的first responder
[self becomeFirstResponder];
// 獲得menu controller單例
UIMenuController *menu = [UIMenuController sharedMenuController];
// 創(chuàng)建menu item,并指定title和action
UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteLine:)];
// 為menu controller 設(shè)置menu items
menu.menuItems = @[deleteItem];
// 為menu controller指定距形
[menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self];
// 將menu controlelr設(shè)置為可見
[menu setMenuVisible:YES animated:YES];
}else{
// 隱藏menu controller
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
}
[self setNeedsDisplay];
}
上面代碼中,首先將view自身設(shè)置為window的first responder,一個自定義view要成為first responder,必須重寫canBecomeFirstResponder方法,并返回YES.
// 重寫canBecomeFirstResponder方法,并返回YES,使得當(dāng)前VIEW可以成為first responder
- (BOOL)canBecomeFirstResponder{
return YES;
}
現(xiàn)在運(yùn)行程序,你會發(fā)現(xiàn)menu并沒有出現(xiàn),因為first responder沒有menu item對應(yīng)的action方法,添加deleteLine方法:
- (void)deleteLine:(id)sender{
[self.finishedLines removeObject:self.selectedLine];
// Redraw everything
[self setNeedsDisplay];
}
UILongPressGestureRecognizer
為BKDrawView添加long press gesture recognizer,默認(rèn)touch持續(xù)0.5秒即為long press,可以修改minimumPressDuration來改變持續(xù)時間。
// 添加監(jiān)聽長按的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。
當(dāng)gesture recognizer狀態(tài)變成非possible時,就會發(fā)送action消息到target,所以在press gesture的開始和結(jié)束狀態(tài),target都會收到action消息。
下面當(dāng)長按屏幕時,選中最近的一條線,當(dāng)長按結(jié)束,釋放選中的線:
- (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
當(dāng)用戶長按線條,然后移動線條,這個動作就叫panning。
通常gesture recognizer不分享其捕獲的touch,一旦識別為了gesture,這些touch不會再被其他gesture recognizer處理。然而pan gesture發(fā)生在long press gesture中,需要這兩種gesture recognizer能夠同時識別gesture。
為了實現(xiàn)這種共享touch,需要實現(xiàn)UIGestureRecognizerDelegate protocol的
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,當(dāng)此方法返回YES,gesture recognizer之間可以分享touches。
pan gesture recognizer支持changed state,當(dāng)手指開始移動,pan recognizer進(jìn)入began state并發(fā)送action消息到target,當(dāng)手指繼續(xù)移動,pan gesture recognizer進(jìn)入changed state并發(fā)送action消息到target。最后,當(dāng)手指離開屏幕,pan gesture recognizer進(jìn)入ended state并發(fā)送最后一次action消息到target。
@interface BKDrawView () <UIGestureRecognizerDelegate>
//@property (nonatomic, strong) BKLine *currentLine;
// 保存當(dāng)前的多個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;
// 創(chuàng)建監(jiān)聽雙擊的gesture recognizer
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
// 當(dāng)touch不再可能被識別為double tap gesture了,再發(fā)送touches began
doubleTapRecognizer.delaysTouchesBegan = YES;
// 將gesture recognizer關(guān)聯(lián)到view上
[self addGestureRecognizer:doubleTapRecognizer];
// 添加監(jiān)聽單擊的gesture recognizer
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
tapRecognizer.delaysTouchesBegan = YES;
// 單擊gesture recognizer要等待雙擊gesture recognizer失敗再觸發(fā)
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
// 添加監(jiān)聽長按的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已經(jīng)移動了多遠(yuǎn)
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移動了多遠(yuǎn),返回的坐標(biāo)CGPoint是在X,Y軸上移動距離的值。
每個UIGestureRecognizer都有cancelsTouchesInView屬性,默認(rèn)值是YES,意思是gesture recognizer會吃掉其識別的touch event,從而不會觸發(fā)UIResponder的方法,如touchesBegan:withEvent:,將其設(shè)置為NO,從而使得touchesMoved:withEvent:可以被執(zhí)行,因為gesture recognizer是攔截器,他控制了是否執(zhí)行UIResponder的方法。
UIResponderStandardEditActions
UIResponderStandardEditActions協(xié)議聲明了UIMenuController中的action方法,如果view實現(xiàn)了這些方法,當(dāng)UIMenuController顯示時,就會顯示對應(yīng)的menu item。
判斷view是否實現(xiàn)了某方法,由canPerformAction:withSender:方法來判斷,menu controller 會發(fā)送該消息給view,默認(rèn)是當(dāng)view實現(xiàn)了某方法,就返回YES,否則返回NO,我們可以重寫該方法。
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{
//return [super canPerformAction:action withSender:sender];
return YES;
}
除了上面所講的gesture recognizer,還有三個內(nèi)置的gesture recognizer: UIPinchGestureRecognizer, UISwipeGestureRecognizer, UIRotationGestureRecognizer。
本文是對《iOS Programming The Big Nerd Ranch Guide 4th Edition》第十三章的總結(jié)。