Event Handling Guide for iOS(二)

手勢識別器

手勢識別器將底層的事件處理代碼轉(zhuǎn)化為高層次的行為父丰。它們是你可以添加到視圖中的對象肝谭,讓你的視圖具備control一樣響應(yīng)操作的能力。手勢識別器將對觸摸進行分析以匹配特定的手勢蛾扇,例如輕掃攘烛、縮放、旋轉(zhuǎn)手勢屁桑。如果他們識別到指派的手勢医寿,它們將發(fā)送動作消息給目標(biāo)對象(target)栏赴。目標(biāo)對象的典型代表是視圖控制器蘑斧,它們對手勢的響應(yīng)可參照圖1-1。這種設(shè)計模式不僅強大而且簡單须眷;你能動態(tài)的決定那個視圖去響應(yīng)操作竖瘾,也不需要因為為視圖添加手勢識別器而創(chuàng)建視圖的子類。

Snip20170907_3.png

通過手勢識別器簡化事件處理

UIKit框架提供了預(yù)先定義的手勢識別器來監(jiān)測一般的手勢花颗。最好能夠使用預(yù)先定義的手勢識別器捕传,因為這能大大的減少你的代碼量。另外扩劝,使用標(biāo)準(zhǔn)的手勢識別器而不是自定義的手勢識別器庸论,能保證你的APP行為在用戶預(yù)期內(nèi)职辅。

如果希望你的APP能識別一種獨一無二的手勢,例如鉤玄聂示、渦旋域携,那么你可以創(chuàng)建你自定義的手勢識別器。學(xué)習(xí)如何設(shè)計和實現(xiàn)自定義的手勢識別器鱼喉,請看"創(chuàng)建自定義的手勢識別器"(在后續(xù)文章中)秀鞭。

內(nèi)置手勢識別器中的常用手勢

當(dāng)你設(shè)計APP時,你需要考慮使用哪種手勢識別器扛禽。對比每一種手勢锋边,下表中的內(nèi)置手勢識別器是否已經(jīng)足夠:


Snip20170908_4.png

你的APP對于手勢的響應(yīng)必須滿足用戶的期望。例如编曼,一個捏合手勢(pinch)就應(yīng)該進行縮放豆巨,而一個點擊手勢(tap)就應(yīng)該是選擇什么內(nèi)容。關(guān)于如何恰如其分的使用手勢的參考灵巧,請查閱iOS Human Interface Guidelines中的“iOS Human Interface Guidelines”搀矫。

添加到視圖中的手勢識別器

每一種手勢識別器都和某一個視圖相關(guān)聯(lián)。另外刻肄,一個視圖可以包含多種手勢識別器瓤球,因為一個單一的視圖可以響應(yīng)許多不同的手勢。如果你希望手勢識別器能夠識別發(fā)生在特定視圖上的觸摸事件敏弃,那么你必須將手勢識別器添加到這個視圖上卦羡。當(dāng)用戶觸摸這個視圖時,手勢識別器會在視圖之前收到一個觸摸的消息麦到。最終绿饵,手勢識別器將代表視圖響應(yīng)觸摸。

手勢觸發(fā)操作消息

當(dāng)手勢識別器識別到指定的手勢瓶颠,便發(fā)送給操作消息到它的目標(biāo)對象拟赊。創(chuàng)建一個手勢識別器需要初始化它的目標(biāo)對象和響應(yīng)方法。

離散的粹淋、連續(xù)的手勢

手勢要么是離散的就是連續(xù)的吸祟。點擊就是離散的手勢,只發(fā)生一次桃移∥葚埃縮放則是連續(xù)的,需要一段時間借杰。對于離散的手勢过吻,手勢識別器向它的目標(biāo)對象發(fā)送單一的操作消息。而連續(xù)的手勢蔗衡,手勢識別器將會持續(xù)的發(fā)送操作消息到目標(biāo)對象纤虽,直到多點觸控序列終止乳绕。 如圖1-2:

Snip20170908_5.png

通過手勢識別器響應(yīng)事件

將內(nèi)置的手勢識別器添加到APP中,你需要做如下三件事:

  • 創(chuàng)建和配置一個手勢識別器實例逼纸,這個步驟包括制定一個目標(biāo)對象刷袍、響應(yīng)方法,以及手勢的一些特有屬性(例如樊展,需要幾根指頭)呻纹。
  • 將手勢識別器添加到視圖。
  • 實現(xiàn)響應(yīng)方法以處理手勢专缠。

通過界面生成器(XIB)添加手勢識別器

Xcode中的界面生成器雷酪,對于添加手勢識別器和添加其他任意對象到界面中的方式是一樣的---從對象庫中拖拽一個手勢識別器到視圖上。這樣操作之后涝婉,手勢是識別器會被自動添加到視圖中去哥力。你可以檢查手勢識別器到底添加到了哪一個視圖當(dāng)中了,如果有必要墩弯,你也可以在NIB文件中修改連接吩跋。

創(chuàng)建手勢識別器對象之后,你需要去建立并連接響應(yīng)方法渔工。這個響應(yīng)方法將會在手勢識別器識別到手勢時調(diào)用锌钮,你也可以創(chuàng)建并連接手勢識別器的關(guān)聯(lián)屬性。你的代碼應(yīng)該參照清單1-1:

清單1-1 通過XIB添加手勢識別器:

@interface APLGestureRecognizerViewController ()
@property (nonatomic, strong) IBOutlet UITapGestureRecognizer *tapRecognizer;
@end
@implementation
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer
     // Will implement method later...
}
@end

通過代碼添加手勢識別器

你可以在代碼中通過配置并初始化一個具體的UIGestureRecognizer子類來創(chuàng)建手勢識別器引矩,例如UIPinchGestureRecognizer梁丘。你可以參照清單1-2來指定目標(biāo)對象和響應(yīng)方法以初始化一個手勢識別器,在大多數(shù)情況下旺韭,目標(biāo)對象會是視圖所在的視圖控制器氛谜。

清單1-2 通過代碼創(chuàng)建一個單擊手勢識別器:

 - (void)viewDidLoad {
     [super viewDidLoad];

  // Create and initialize a tap gesture
       UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
            initWithTarget:self action:@selector(respondToTapGesture:)];
       // Specify that the gesture must be a single tap
       tapRecognizer.numberOfTapsRequired = 1;
       // Add the tap gesture recognizer to the view
       [self.view addGestureRecognizer:tapRecognizer];
       // Do any additional setup after loading the view, typically from a nib
  }

響應(yīng)離散手勢

當(dāng)你創(chuàng)建一個手勢識別器,你會將它與一個響應(yīng)方法進行連接区端。利用這個響應(yīng)方法來響應(yīng)手勢識別器對應(yīng)的手勢值漫。清單1-3展示了一個響應(yīng)離散手勢的例子。當(dāng)用戶點擊了添加了手勢識別器的視圖织盼,控制器將會顯示一個圖片以示發(fā)生了點擊杨何。showGestureForTapRecognizer:方法通過手勢識別器的locationInView:方法來確定手勢在視圖中的位置,并將圖片顯示在這個位置上悔政。

接下來的三段式里代碼來自于Simple Gesture Recognizers示例工程晚吞,你可以查看該工程獲取更多信息延旧。

清單 1-3 處理一個雙擊手勢

- (IBAction)showGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer {
         // Get the location of the gesture
        CGPoint location = [recognizer locationInView:self.view];
         // Display an image view at that location
        [self drawImageForGestureRecognizer:recognizer atPoint:location];
         // Animate the image view so that it fades out
      [UIView animateWithDuration:0.5 animations:^{
             self.imageView.alpha = 0.0;
      }];
 }

每一種手勢識別器都有自己特有的屬性列表谋国。例如,在清單1-4中迁沫,showGestureForSwipeRecognizer:方法使用了輕掃手勢識別器的方法(direction)屬性來確定用戶是向左還是向右輕掃芦瘾。然后捌蚊,利用該屬性的值將圖片在輕掃的方向上淡出。

清單 1-4 響應(yīng)向右或向左的輕掃手勢

// Respond to a swipe gesture
- (IBAction)showGestureForSwipeRecognizer:(UISwipeGestureRecognizer *)recognizer
{
}
// Get the location of the gesture
CGPoint location = [recognizer locationInView:self.view];
// Display an image view at that location
[self drawImageForGestureRecognizer:recognizer atPoint:location];
// If gesture is a left swipe, specify an end location
// to the left of the current location
if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
     location.x -= 220.0;
} else {
     location.x += 220.0;
}
// Animate the image view in the direction of the swipe as it fades out
[UIView animateWithDuration:0.5 animations:^{
     self.imageView.alpha = 0.0;
     self.imageView.center = location;
}];

響應(yīng)連續(xù)的手勢

連續(xù)的手勢允許你的APP響應(yīng)正在發(fā)生的手勢近弟。例如缅糟,你的APP能在用戶捏合(pinch)的時候不斷的進行縮放,也可在用戶拖拽的時候圍繞著屏幕運動祷愉。

清單1-5展示了一個與手勢相同角度不斷旋轉(zhuǎn)的圖片窗宦,并且當(dāng)用戶停止轉(zhuǎn)動時,讓圖片旋轉(zhuǎn)回到水平二鳄,與此同時將圖片進行動畫淡出赴涵。當(dāng)用戶旋轉(zhuǎn)指頭,showGestureForRotationRecognizer:方法會被持續(xù)的調(diào)用直到所有的手指離開屏幕订讼。

清單 1-5 響應(yīng)旋轉(zhuǎn)手勢

// Respond to a rotation gesture
- (IBAction)showGestureForRotationRecognizer:(UIRotationGestureRecognizer
*)recognizer {
       // Get the location of the gesture
       CGPoint location = [recognizer locationInView:self.view];
       // Set the rotation angle of the image view to
       // match the rotation of the gesture
       CGAffineTransform transform = CGAffineTransformMakeRotation([recognizer
rotation]);
       self.imageView.transform = transform;
       // Display an image view at that location
       [self drawImageForGestureRecognizer:recognizer atPoint:location];
      // If the gesture has ended or is canceled, begin the animation
      // back to horizontal and fade out
      if (([recognizer state] == UIGestureRecognizerStateEnded) || ([recognizer
state] == UIGestureRecognizerStateCancelled)) {
           [UIView animateWithDuration:0.5 animations:^{
                self.imageView.alpha = 0.0;
                self.imageView.transform = CGAffineTransformIdentity;
            }];
       }
}

每當(dāng)showGestureForRotationRecognizer: 方法被調(diào)用髓窜,在drawImageForGestureRecognizer:方法中,圖片都會被設(shè)置為不透明的欺殿。當(dāng)手勢結(jié)束寄纵,圖片在animateWithDuration: 方法中被設(shè)置為透明的。showGestureForRotationRecognizer:根據(jù)校對手勢識別器的狀態(tài)確定了手勢是否已經(jīng)完成脖苏。手勢識別器的狀態(tài)會在稍后的文章中進行詳細(xì)的說明程拭。

定義手勢的交互方式

一般來說,當(dāng)你添加手勢識別器到你的APP棍潘,你需要對手勢識別器如何進行交互以及觸控事件的處理代碼有明確的期望哺壶。要做到這一點,你首先應(yīng)該充分理解手勢識別器是如何工作的蜒谤。

手勢識別器在有限的狀態(tài)機下運轉(zhuǎn)

手勢識別器按照預(yù)先定義的方式從一種狀態(tài)過渡到另外一種狀態(tài)山宾。手勢識別器會根據(jù)遇到的確切條件從一種狀態(tài)切換到任何一種可能的狀態(tài)下。精準(zhǔn)的狀態(tài)機變化依賴于手勢識別器是離散的還是連續(xù)的鳍徽,參見圖1-3资锰。所有的手勢識別器處于可能的狀態(tài)(UIGestureRecognizerStatePossible)。他們分析收到的任何多點觸控序列阶祭,在分析過程中要么識別失敗就識別成功绷杜。識別識別以為這手識別器切換到失敗狀態(tài)(UIGestureRecognizerStateFailed)。

圖1-3 手勢識別器狀態(tài)機

Snip20170911_6.png

當(dāng)一個離散的手勢識別器識別到手勢濒募,他的狀態(tài)由可能(Possible)變?yōu)樽R別到的(UIGestureRecognizerStateRecognized)鞭盟,并且識別過程結(jié)束。

對于連續(xù)的手勢瑰剃,手勢識別器的狀態(tài)在第一次識別到手勢時由可能變?yōu)殚_始(UIGestureRecognizerStateBegan)齿诉。然后變?yōu)楦淖兊模?em>UIGestureRecognizerStateChanged),并且將不斷的由改變的轉(zhuǎn)換為改變的宁赤。當(dāng)用戶最后的手指離開視圖皂冰,狀態(tài)變?yōu)榻Y(jié)束的(UIGestureRecognizerStateEnded)并且手勢識別結(jié)束。注意滔吠,結(jié)束狀態(tài)是識別到的(UIGestureRecognizerStateRecognized)別名而已抵恋。

如果連續(xù)手勢的識別器判定手勢不滿足期望的模式焕议,識別器也能從改變的狀態(tài)變化為取消狀態(tài)(UIGestureRecognizerStateCancelled)。

每當(dāng)手勢識別器的狀態(tài)改變弧关,他都將發(fā)送動作消息到他的目標(biāo)對象盅安,除非狀態(tài)改變?yōu)槭』蛘呷∠R虼艘粋€離散的手勢識別器只發(fā)送單個動作消息世囊,當(dāng)狀態(tài)由可能變?yōu)樽R別到的宽堆。而一個連續(xù)的手勢識別器在狀態(tài)改變時會發(fā)送很多動作消息。

當(dāng)手勢識別器的狀態(tài)變?yōu)樽R別到的(結(jié)束的)狀態(tài)茸习,便重置狀態(tài)到可能的(Possible)畜隶,此過程不發(fā)送動作消息。

與其他手勢識別器交互

一個視圖可以添加多個手勢識別器号胚。使用視圖的gestureRecognizers屬性來決定哪個手勢被添加到視圖中去籽慢。你可以動態(tài)的改變一個視圖如何處理手勢,分別通過addGestureRecognizer:removeGestureRecognizer:方法來添加和移除手勢猫胁。

當(dāng)一個視圖添加了多點觸控手勢識別器箱亿,你可能希望改變手識別器對于觸摸事件接收和分析的競爭方式。在默認(rèn)情況下弃秆,并沒有設(shè)定的順序規(guī)定哪個手勢識別器最先收到觸摸事件届惋,因為這個原因,觸摸事件每次會以不同的順序發(fā)送給手勢識別器菠赚。你可以覆寫這一默認(rèn)的行為:

  • 指定一個手勢識別器最先收到和分析觸摸事件脑豹。
  • 允許兩個手勢同時運作。
  • 阻止一個手勢識別器進行觸摸事件的接收和分析衡查。

子類通過實現(xiàn)UIGestureRecognizer的類方法瘩欺、代理方法,和覆寫父類方法來實現(xiàn)這些行為拌牲。

聲明兩個手勢識別器的特定順序

想象你希望識別輕掃(Swipe)和拖動(Pan)手勢俱饿,并且你希望他們觸發(fā)不同的操作。在默認(rèn)情況下塌忽,當(dāng)用戶試圖進行輕掃拍埠,這個手勢會被解釋為拖動(Pan)。因為輕掃手勢在滿足被解釋為輕掃手勢(離散的手勢)的條件之前滿足了被解釋為拖動手勢(連續(xù)的手勢)的條件土居。

如果視圖同時能夠識別輕掃和拖動嬉探,而你希望輕掃手勢識別器在拖動手勢識別器之前分析觸摸事件。如果輕掃手勢識別器判定觸摸是輕掃埂奈,那么拖動手勢識別器永遠(yuǎn)不需要去分析觸摸, 如果輕掃手勢識別器判定觸摸不是輕掃芹敌,他會變?yōu)槭顟B(tài)垮抗,并且拖動手勢識別器應(yīng)該開始分析觸摸事件。

你通過將想要延遲分析事件的手勢調(diào)用requireGestureRecognizerToFail:方法來聲明兩個手勢識別器的關(guān)系液茎,參見清單1-6。在這份清單中辞嗡,兩個手勢識別器被添加到同一個視圖捆等。

清單1-6 拖動手勢識別器需要輕掃手勢識別器識別失敗

- (void)viewDidLoad {
       [super viewDidLoad];

      // Do any additional setup after loading the view, typically from a nib
      [self.panRecognizer requireGestureRecognizerToFail:self.swipeRecognizer];
}

requireGestureRecognizerToFail:方法向消息接收者(理解為該方法的調(diào)用者)發(fā)送消息規(guī)定只有當(dāng)其他的手勢識別器識別失敗后接收者才能開始識別和分析觸摸事件。在等待其他手勢識別器狀態(tài)變?yōu)槭〉倪^程當(dāng)中续室,接收者的狀態(tài)始終為可能栋烤。如果其他的手勢識別器識別失敗,接收者的狀態(tài)會發(fā)生改變挺狰。另一方面明郭,如果其他的手勢識別器狀態(tài)變?yōu)樽R別到的或者開始,接收者手勢識別器狀態(tài)將變?yōu)槭顟B(tài)丰泊。更多關(guān)于狀態(tài)變換的信息薯定,請看"手勢識別器在有限的狀態(tài)機下運轉(zhuǎn)"。

注意:如果你的APP同時能夠識別單擊和雙擊手勢并且單擊手勢識別器不需要雙擊手勢識別器的失敗瞳购,此外你希望在雙擊操作之前收到單擊操作沉唠,盡管用戶進行了雙擊。 這種行為是有意而為的苛败,因為通常最好的用戶體驗要能支持多種類型的操作满葛。
如果你希望這兩種操作是互斥的,你的單擊手勢識別器應(yīng)該需要雙擊手勢識別器的失敗罢屈。但是嘀韧,單擊響應(yīng)的操作會稍微滯后于用戶的輸入,因為單擊手勢識別器被延遲到雙擊手勢識別器失敗之后缠捌。

阻止手勢識別器分析觸摸事件

你可以通過為手勢識別器添加代理對象來改變它的行為锄贷。UIGestureRecognizerDelegate協(xié)議提供了兩個方法來供你阻止手勢識別器分析觸摸事件译蒂。你可以使用gestureRecognizer:shouldReceiveTouch:方法或者gestureRecognizerShouldBegin:方法---都是UIGestureRecognizerDelegate協(xié)議的可選實現(xiàn)方法柔昼。

當(dāng)觸摸開始捕透,如果你能夠立刻判定你的手勢識別器是否應(yīng)該考慮這次觸摸,那么你可以使用gestureRecognizer:shouldReceiveTouch:方法虎谢。這個方法會在每次發(fā)生觸摸的時候被調(diào)用婴噩。一個觸摸發(fā)生時,返回NO的手勢識別器不會收到事件的通知银觅。

清單1-7 通過gestureRecognizer:shouldReceiveTouch:代理方法阻止一個自定義視圖的單擊手勢識別器收到觸摸事件。 當(dāng)觸摸發(fā)生洒忧,gestureRecognizer:shouldReceiveTouch:方法別調(diào)用熙侍。它判斷用戶是否觸摸了自定義視圖蛉抓,如果是的,則阻止單擊手勢識別器收到觸摸事件笑跛。

清單1-7 阻止手勢識別器收到觸摸事件

- (void)viewDidLoad {
    [super viewDidLoad];
    // Add the delegate to the tap gesture recognizer
    self.tapGestureRecognizer.delegate = self;
}


// Implement the UIGestureRecognizerDelegate method
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch {
    // Determine if the touch is inside the custom subview
    if ([touch view] == self.customSubview)){
        // If it is, prevent all of the delegate's gesture recognizers
        // from receiving the touch
        return NO;
}
return YES; }

如果你需要等待足夠長的時間才能決定一個手勢識別器是否應(yīng)該分析一個觸摸几苍,那么你可以使用* gestureRecognizerShouldBegin:* 代理方法妻坝。一般情況下赚抡,如果你在UIView或者UIControl的子類中自定義事件處理并且存在手勢識別器競爭涂臣,那么你可以使用該方法署辉。返回NO將導(dǎo)致手勢識別器立馬識別失敗哭尝,允許其他手勢識別器繼續(xù)進行觸摸處理。如果手勢識別要阻止視圖或者控件收到觸摸事件桶唐,這個方法會在手勢識別器試圖離開可能(Possible)狀態(tài)時被調(diào)用尤泽。

當(dāng)你的視圖或者視圖控制器不能作為手勢識別器的代理時,你也能直接使用 (gestureRecognizerShouldBegin:UIView)方法莫鸭。該方法的簽名和實現(xiàn)一樣妇智。

允許同時進行手勢識別

默認(rèn)情況下巍棱,兩個手勢識別器不能同時識別他們各自的手勢航徙。但是假設(shè)你希望用戶能夠同時對視圖進行縮放和旋轉(zhuǎn)杠袱。你需要通過實現(xiàn)gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法來改變原本的默認(rèn)行為楣富,該方法為UIGestureRecognizerDelegateprotocol協(xié)議的可選實現(xiàn)方法。當(dāng)一個手勢識別器器分析一個手勢而且會阻止另一個手勢識別器識別該手勢時踪少,此方法會被調(diào)用兼犯,反之亦然切黔。此方法默認(rèn)返回NO。當(dāng)你希望兩個手勢識別器同時進行手勢分析時返回YES险领。

注意:只有在任何一個手勢識別器允許同時進行手勢識別的情況下绢陌,你需要實現(xiàn)這個代理方法并返回YES。然而秤掌,這也意味著返回NO不一定能夠阻止同時識別手勢闻鉴,因為其他手勢識別器的代理可能會返回YES瓶竭。

指定兩個手勢識別器的單向關(guān)系

如果你想控制兩個手勢識別器之間是如何交互的,你需要指定一個單向關(guān)系次询,你可以在子類中覆寫* canPreventGestureRecognizer:或者canBePreventedByGestureRecognizer:*方法來返回NO(默認(rèn)返回YES)块饺。例如,如果你希望旋轉(zhuǎn)手勢阻止捏合手勢世落,而不希望捏合手勢阻止旋轉(zhuǎn)手勢屉佳,你可以這樣指定單向關(guān)系:

/// 我的備注武花,前面說到可以允許兩個手勢同時進行識別,而一般來說兩個手勢的識別順序并不能確定累铅,這里就可以滿足進行旋轉(zhuǎn)識別時不進行捏合識別,而進行捏合識別時也會進行旋轉(zhuǎn)識別尽楔。
 [rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];

并且覆寫旋轉(zhuǎn)手勢識別器子類方法來返回NO玛荞。關(guān)于如何實現(xiàn)UIGestureRecognizer子類驹碍,參見"創(chuàng)建自定義手勢識別器"(后序文章)。

如果兩個手勢并不存在互相的阻止浮还,使用gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,參見"允許同時進行手勢識別"洼冻。默認(rèn)情況下,一個捏合手勢阻止一個旋轉(zhuǎn)手勢屋彪,反之亦然,因為兩個手勢不能被同時識別蟹但。

與用戶界面控件交互

iOS6.0及其以后,默認(rèn)的控件操作會防止重疊的手勢識別器行為口糕。例如十办,按鈕的默認(rèn)操作是單擊呵燕。如果你向按鈕的父視圖添加了單擊手勢識別器,并點擊了按鈕夜矗,按鈕的響應(yīng)方法會收到觸摸事件而不是手勢識別器。這種情況出現(xiàn)在手勢識別器與默認(rèn)的控件操作重疊時对扶,例如:

  • 單個手指點擊一個UIButton, UISwitch, UIStepper, UISegmentedControl, 和UIPageControl区赵。
  • 單個手指在與UISlider平行的方向上輕掃了UISlider的按鈕。
  • 單個手指在與UISwitch平行的方向上拖動了UISwitch的按鈕浪南。

如果你自定義了任何一種的控件的子類并希望改變這種默認(rèn)的行為笼才,直接向控件添加手勢識別器而不是它的父視圖。然后逞泄,這個手勢識別器會率先收到觸摸事件患整。一如既往的拜效,請確保你閱讀了 iOS Human Interface Guidelines以保證你的APP提供直觀的用戶體驗憔四,尤其是在覆寫標(biāo)準(zhǔn)控件的默認(rèn)行為時。

手勢識別器解釋原生的觸摸事件

到目前為止,你已經(jīng)了解了手勢以及你的APP如何去識別和響應(yīng)他們。 然而膀跌,如何去自定義手勢識別器或者如何讓手勢識別器與視圖的觸摸事件處理交互困介,你需要在觸摸和事件上面思考得更多导坟。

一個事件包含了當(dāng)前多點觸控序列的所有觸摸

在iOS中,一個觸摸代表了一個手指在屏幕上的移動。一個手勢有一個或者多個觸摸核芽,觸摸用UITouch對象表示拳芙。例如羡疗,一個捏縮手勢有兩個觸摸---有兩個手指在屏幕上從不同的方向朝各自移動痢毒。

一個事件(Event)包含了多點觸控序列過程中所有的觸摸库快。一個多點觸控序列在一個手指觸摸到屏幕時開始,在最后一個手指離開屏幕時結(jié)束沐悦。當(dāng)一個手指移動時副签,iOS發(fā)送UITouch對象給事件。一個多點觸控事件表示了一個UIEventTypeTouches類型的UIEvent對象。

每一個觸摸對象僅僅追蹤一個手指拯钻,并與多點觸控序列的生命周期相同。在整個序列過程中,UIKit追蹤手指并更新觸摸對象的屬性矫夯。這些屬性包括階段拘领、在視圖中的位置送悔、上一個位置和時間戳。

觸摸的階段表明了觸摸何時開始,在移動還是靜止,以及何時結(jié)束(意味著手指不再觸摸屏幕)晦款。如圖1-4的描述义锥,一個APP會在觸摸的不同階段受到事件(Event)對象柳沙。

圖1-4 多點觸控序列和觸摸階段

Snip20170912_10.png

注意:一個手指并沒有鼠標(biāo)精確。當(dāng)用戶觸摸屏幕拌倍,接觸的區(qū)域其實是橢圓形的而且比我們想象中的位置偏低赂鲤。這個接觸區(qū)域的變化取決于手指的尺寸、方向柱恤、壓力数初、哪一根手指等因素。底層的多點觸控系統(tǒng)幫你分析和計算了觸摸點梗顺,因此你不需要自己編寫代碼來實現(xiàn)這些泡孩。

APP在觸摸處理方法中接收觸摸

在多點觸控序列過程中,如果在特定的觸摸階段產(chǎn)生了新的觸摸或者觸摸發(fā)生了改變荚守,APP將會發(fā)送觸摸消息珍德。會調(diào)用如下方法:

上面的每一個方法都與一個觸摸階段相關(guān)聯(lián)泵琳;例如,touchesBegan:withEvent:方法和UITouchPhaseBegan相關(guān)聯(lián)誊役。觸摸階段保存在觸摸對象的phase屬性當(dāng)中获列。

注意:這些方法并不和手勢識別器的狀態(tài)相關(guān)聯(lián),例如UIGestureRecognizerStateBeganUIGestureRecognizerStateEnded蛔垢。手勢識別器的狀態(tài)充分的表明了其本身的階段击孩,而不是識別到的觸摸對象的階段。

調(diào)節(jié)發(fā)送到視圖的觸摸

或許在某些情況下鹏漆,你希望一個視圖在手勢識別器之前收到觸摸巩梢。但是,在你能夠改變觸摸傳遞到視圖的路徑之前艺玲,你需要理解默認(rèn)的行為括蝠。在這個簡單的例子中,當(dāng)一個觸摸發(fā)生饭聚,觸摸對象從UIApplication對象傳遞到UIWindow對象(我的備注:實際上傳遞的是UIEvent對象)忌警。然后,窗口(window)對象首先將觸摸發(fā)送到被添加到觸摸點發(fā)生處的視圖(或者它的父視圖)上的手勢識別器秒梳,在發(fā)送給視圖本身之前法绵。

圖1-5 默認(rèn)的觸摸事件傳遞步驟

Snip20170912_11.png

手勢識別器首先識別觸摸

一個窗口(window)對象延遲發(fā)送觸摸對象給視圖箕速,為了手勢識別器能首先分析觸摸。在延遲過程中礼烈,如果手勢識別器識別到了觸摸手勢弧满,那么窗口(window)對象永遠(yuǎn)不會將觸摸對象發(fā)送給視圖婆跑,并且會取消任何之前發(fā)送給視圖的觸摸對象---識別的序列的一部分此熬。

例如,你有一個需要兩個手指觸摸的離散手勢識別器滑进,觸摸會被轉(zhuǎn)換成兩個不同的觸摸對象犀忱。當(dāng)觸摸發(fā)生,觸摸點視圖的觸摸對象從APP對象發(fā)送到window對象扶关,并且接下來發(fā)生的序列阴汇,如圖1-6描述:

圖1-6 觸摸對象的消息序列

Snip20170912_12.png
  1. 窗口(window)對象在開始階段發(fā)送兩個觸摸對象---通過touchesBegan:withEvent:方法發(fā)送給手勢識別器。 手勢識別器還沒有完成手勢的識別节槐,因此它的狀態(tài)仍是可能(Possible)搀庶。窗口對象會將同樣的觸摸對象發(fā)送給手勢識別器相關(guān)聯(lián)的視圖。
  2. 窗口(window)對象在移動階段發(fā)送兩個觸摸對象---通過touchesMoved:withEvent:方法發(fā)送給手勢識別器铜异。 手勢識別器還沒有完成手勢的識別哥倔,因此它的狀態(tài)仍是可能(Possible)。窗口對象會將同樣的觸摸對象發(fā)送給手勢識別器相關(guān)聯(lián)的視圖揍庄。
  3. 窗口(window)對象在結(jié)束階段發(fā)送一個觸摸對象---通過touchesEnded:withEvent:方法發(fā)送給手勢識別器咆蒿。 這個觸摸對象沒有包含足夠的手勢信息,但是窗口(window)對象不發(fā)送該對象到視圖蚂子。
  4. 窗口(window)對象在結(jié)束階段發(fā)送另一個觸摸對象沃测。此時手勢識別器識別到了手勢,因此其狀態(tài)變?yōu)樽R別到的食茎。在第一個操作消息發(fā)送前蒂破,視圖調(diào)用touchesCancelled:withEvent:方法去取消之前在開始和移動階段發(fā)送的觸摸對象。觸摸在結(jié)束階段被取消掉(注意:touchesEnded:withEvent:并不會被調(diào)用)别渔。

現(xiàn)在假設(shè)手勢識別器在最后一步判定多點觸控序列的分析結(jié)果并非自己的手勢附迷。它將狀態(tài)設(shè)置為UIGestureRecognizerStateFailed。然后窗口對象通過* touchesEnded:withEvent:*消息體發(fā)送兩個觸摸對象給關(guān)聯(lián)的視圖钠糊。

一個連續(xù)的手勢識別器遵循相似的序列挟秤,除非它可能在結(jié)束階段前識別到手勢〕椋快要識別到手勢前艘刚,他的狀態(tài)變?yōu)?em>UIGestureRecognizerStateBegan(而不是識別到的)。窗口對象(window)發(fā)送隨后的多點觸控序列中的觸摸對象給手勢識別器而不是關(guān)聯(lián)的視圖截珍。

改變觸摸到視圖的發(fā)送

你可通過修改某些UIGestureRecognizer的屬性來改變默認(rèn)的傳遞路徑攀甚。如果你改變了這些屬性值箩朴,你會得到如下不同的行為:

  • delaysTouchesBegan(默認(rèn)為NO)---正常情況下,窗口對象會發(fā)送開始和移動階段的觸摸對象給視圖和手勢識別器秋度。設(shè)置delaysTouchesBegan為YES阻止窗口發(fā)送開始和移動階段的觸摸對象給視圖炸庞。這樣能夠保證當(dāng)一個手勢識別器識別手勢時,不會有任何觸摸對象發(fā)送給關(guān)聯(lián)的視圖荚斯。慎重使用該屬性埠居,因為它會導(dǎo)致你的用戶界面看起來反應(yīng)遲鈍。

  • delaysTouchesEnded(默認(rèn)為YES)---當(dāng)這個屬性被設(shè)置為YES事期,它確保視圖不會完成一個手勢想要取消的動作滥壕。當(dāng)一個手勢識別器正在識別觸摸事件,窗口對象不會將結(jié)束階段的觸摸對象發(fā)送給給關(guān)聯(lián)的視圖兽泣。如果一個手勢識別器識別到了手勢绎橘,這個觸摸對象將會被取消。如果手勢識別器沒有識別到手勢唠倦,窗口對象會通過delaysTouchesEnded消息發(fā)送觸摸對象到關(guān)聯(lián)的視圖称鳞。設(shè)置該屬性值為NO以允許視圖和手勢識別器同時分析結(jié)束階段的觸摸對象。

    考慮到一種情況稠鼻,即一個視圖有一個需要兩個手指 點擊的手勢識別器冈止,并且用戶雙擊了視圖。該屬性值被設(shè)置為YES枷餐,視圖會收到如下消息touchesBegan:withEvent:, touchesBegan:withEvent:, touchesCancelled:withEvent:, 和touchesCancelled:withEvent:靶瘸。如果該屬性值被設(shè)置為NO,視圖會收到如下序列消息:touchesBegan:withEvent:, touchesEnded:withEvent:, touchesBegan:withEvent:, 和 touchesCancelled:withEvent:,這意味著在方法touchesBegan:withEvent:中毛肋,視圖能夠識別雙擊怨咪。(我的備注:我自己的測試和上述效果并不一致)

一個手勢識別器監(jiān)測一個觸摸并判斷他是否為手勢的一部分,他能直接將觸摸傳遞給視圖润匙。要做到這樣诗眨,手勢識別器自己調(diào)用ignoreTouch:forEvent:,傳遞觸摸對象孕讳。

PS:這一章沒怎么看明白匠楚。--

后序研究

cancelsTouchesInView屬性默認(rèn)值為YES,當(dāng)手勢識別器識別到手勢時會調(diào)用厂财。
touchesCancelled:withEvent:方法芋簿,而touchesEnded:withEvent:不會調(diào)用,只有當(dāng)識別失敗才會調(diào)用璃饱。
如果cancelsTouchesInView被設(shè)置為NO与斤,設(shè)置delaysTouchesEnded并無作用。

創(chuàng)建自定義手勢識別器

為了實現(xiàn)自定義的手勢識別器,首先需要創(chuàng)建一個UIGestureRecognizer的子類撩穿。然后在子類的頭文件中添加如下導(dǎo)入命令:

 #import <UIKit/UIGestureRecognizerSubclass.h>

接下來磷支,從UIGestureRecognizerSubclass.h頭文件中拷貝如下方法聲明到你的頭文件中,這些是你需要在子類中實現(xiàn)的方法:

- (void)reset;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

這些方法和早前在"APP在觸摸處理方法中接收觸摸"(在前面內(nèi)容中)中描述的方法具備相同的方法簽名和行為食寡。所有你在子類中覆寫的方法都必須調(diào)用父類實現(xiàn)雾狈,盡管父類只有方法的空實現(xiàn)。

注意UIGestureRecognizerSubclass.h中的state屬性現(xiàn)在是可讀可寫的而不僅僅只是可讀抵皱,你在子類中使用UIGestureRecognizerState枚舉來設(shè)置state屬性值善榛。

實現(xiàn)自定義手勢識別器的觸摸事件處理方法

實現(xiàn)自定義手勢識別器的四個核心方法為:touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, 和touchesCancelled:withEvent:。利用這些方法叨叙,你可以通過設(shè)置手勢識別器的狀態(tài)(state)將底層的事件處理轉(zhuǎn)換為高層的手勢識別锭弊。清單1-8 創(chuàng)建了一個手別識別器,具備離散的單擊勾選手勢擂错。它記錄了手勢的中點---上行運動開始的地方---而客戶端可以獲取這個值。

這個例子只有一個視圖樱蛤,但是大多數(shù)APP擁有許多視圖钮呀。一般情況下,你需要將觸摸的位置轉(zhuǎn)換為相對于屏幕的坐標(biāo)昨凡,你才能正確的識別拖動視圖的手勢爽醋。

清單1-8 實現(xiàn)勾選手勢識別器

#import <UIKit/UIGestureRecognizerSubclass.h>
 
// Implemented in your custom subclass
// Implemented in your custom subclass
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if ([touches count] != 1) {
        self.state = UIGestureRecognizerStateFailed;
        return; }
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
   
//    CGPoint  nowPoint = [touches.anyObject locationInView:self.view];
//    
//    CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view];
    
    /// 另外一種寫法
    CGPoint  nowPoint = [touches.anyObject locationInView:self.view.window];
    
    CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view.window];
    
    // strokeUp is a property
    if (!self.strokeUp) {
        // On downstroke, both x and y increase in positive direction
        if (nowPoint.x >= prevPoint.x && nowPoint.y >= prevPoint.y) {
            self.midPoint = nowPoint;
            // Upstroke has increasing x value but decreasing y value
        } else if (nowPoint.x >= prevPoint.x && nowPoint.y <= prevPoint.y) {
            self.strokeUp = YES;
        } else {
            self.state = UIGestureRecognizerStateFailed;
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    if ((self.state == UIGestureRecognizerStatePossible) && self.strokeUp) {
        self.state = UIGestureRecognizerStateRecognized;
        
        NSLog(@"識別到手勢");
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
    self.state = UIGestureRecognizerStateFailed;
}

離散手勢和連續(xù)手勢的狀態(tài)切換時不同的,在前面的內(nèi)容中"手勢識別器在有限狀態(tài)機下運轉(zhuǎn)"有描述便脊。當(dāng)你創(chuàng)建一個自定義的手勢識別器蚂四,你通過設(shè)置相關(guān)的狀態(tài)來聲明它是離散的還是連續(xù)的。作為示例哪痰,清單1-8的勾選手勢識別器沒有將狀態(tài)設(shè)置為開始的(Begin)或者改變的(Changed)遂赠,因為它是離散的。

當(dāng)你實現(xiàn)一個子類的手勢識別器是晌杰,最重要的事情是你需要正確的設(shè)置手勢識別器的狀態(tài)(state)跷睦。iOS系統(tǒng)需要知道手勢識別器的狀態(tài)以讓他按照預(yù)期那樣交互。例如肋演,如果你需要同時進行手勢識別或者需要某個手勢識別器失敗抑诸,iOS需要明白你當(dāng)前手勢的狀態(tài)。

關(guān)于更多自定義手勢識別器爹殊,請參見WWDC 2012: Building Advanced Gesture Recognizers

重置手勢識別器的狀態(tài)

如果你的手勢識別器變?yōu)樽R別到的蜕乡、結(jié)束的、失敗的梗夸、或者取消的层玲,UIGestureRecognizer會在返回到可能(Possible)狀態(tài)前調(diào)用reset方法。

清單1-9,實現(xiàn)了reset方法來重置手勢識別器內(nèi)部的各種狀態(tài)以準(zhǔn)備好識別新的手勢称簿。手勢識別器返回了這個方法后扣癣,他將不會收到進程中觸摸對象進一步更新。

清單1-9 重置手勢識別器

- (void)reset {
    [super reset];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末憨降,一起剝皮案震驚了整個濱河市父虑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌授药,老刑警劉巖士嚎,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異悔叽,居然都是意外死亡莱衩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門娇澎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笨蚁,“玉大人,你說我怎么就攤上這事趟庄±ㄏ福” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵戚啥,是天一觀的道長奋单。 經(jīng)常有香客問我,道長猫十,這世上最難降的妖魔是什么览濒? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮拖云,結(jié)果婚禮上贷笛,老公的妹妹穿的比我還像新娘。我一直安慰自己江兢,他們只是感情好昨忆,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杉允,像睡著了一般邑贴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叔磷,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天拢驾,我揣著相機與錄音,去河邊找鬼改基。 笑死繁疤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稠腊,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼躁染,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了架忌?” 一聲冷哼從身側(cè)響起吞彤,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叹放,沒想到半個月后饰恕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡井仰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年埋嵌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俱恶。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡雹嗦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出速那,到底是詐尸還是另有隱情俐银,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布端仰,位于F島的核電站,受9級特大地震影響田藐,放射性物質(zhì)發(fā)生泄漏荔烧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一汽久、第九天 我趴在偏房一處隱蔽的房頂上張望鹤竭。 院中可真熱鬧,春花似錦景醇、人聲如沸臀稚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吧寺。三九已至,卻和暖如春散劫,著一層夾襖步出監(jiān)牢的瞬間稚机,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工获搏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赖条,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像纬乍,于是被迫代替她去往敵國和親碱茁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的仿贬?困惑于Cell怎么突然不能點擊了纽竣?糾結(jié)于如何實現(xiàn)這個奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 56,800評論 51 597
  • 手勢識別器是附加到視圖的對象诅蝶,將低級別事件處理代碼轉(zhuǎn)換為更高級別的操作退个,它允許視圖以控件執(zhí)行的方式響應(yīng)操作。 手勢...
    坤坤同學(xué)閱讀 4,067評論 0 9
  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件调炬。本想自己總結(jié)一下语盈,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位缰泡,特此轉(zhuǎn)載刀荒。作者:L...
    WQ_UESTC閱讀 5,992評論 4 26
  • 在開發(fā)過程中,大家或多或少的都會碰到令人頭疼的手勢沖突問題棘钞,正好前兩天碰到一個類似的bug缠借,于是借著這個機會了解了...
    閆仕偉閱讀 5,301評論 2 23
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點觸摸的強大功能,喬布斯讓人們認(rèn)識到手機其實...
    翹楚iOS9閱讀 2,947評論 0 13