Event Handling Guide for iOS(二)

手勢識別器

手勢識別器將底層的事件處理代碼轉化為高層次的行為。它們是你可以添加到視圖中的對象,讓你的視圖具備control一樣響應操作的能力。手勢識別器將對觸摸進行分析以匹配特定的手勢,例如輕掃、縮放、旋轉手勢。如果他們識別到指派的手勢,它們將發送動作消息給目標對象(target)。目標對象的典型代表是視圖控制器,它們對手勢的響應可參照圖1-1。這種設計模式不僅強大而且簡單;你能動態的決定那個視圖去響應操作,也不需要因為為視圖添加手勢識別器而創建視圖的子類。

Snip20170907_3.png

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

UIKit框架提供了預先定義的手勢識別器來監測一般的手勢。最好能夠使用預先定義的手勢識別器,因為這能大大的減少你的代碼量。另外,使用標準的手勢識別器而不是自定義的手勢識別器,能保證你的APP行為在用戶預期內。

如果希望你的APP能識別一種獨一無二的手勢,例如鉤玄、渦旋,那么你可以創建你自定義的手勢識別器。學習如何設計和實現自定義的手勢識別器,請看"創建自定義的手勢識別器"(在后續文章中)。

內置手勢識別器中的常用手勢

當你設計APP時,你需要考慮使用哪種手勢識別器。對比每一種手勢,下表中的內置手勢識別器是否已經足夠:


Snip20170908_4.png

你的APP對于手勢的響應必須滿足用戶的期望。例如,一個捏合手勢(pinch)就應該進行縮放,而一個點擊手勢(tap)就應該是選擇什么內容。關于如何恰如其分的使用手勢的參考,請查閱iOS Human Interface Guidelines中的“iOS Human Interface Guidelines”。

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

每一種手勢識別器都和某一個視圖相關聯。另外,一個視圖可以包含多種手勢識別器,因為一個單一的視圖可以響應許多不同的手勢。如果你希望手勢識別器能夠識別發生在特定視圖上的觸摸事件,那么你必須將手勢識別器添加到這個視圖上。當用戶觸摸這個視圖時,手勢識別器會在視圖之前收到一個觸摸的消息。最終,手勢識別器將代表視圖響應觸摸。

手勢觸發操作消息

當手勢識別器識別到指定的手勢,便發送給操作消息到它的目標對象。創建一個手勢識別器需要初始化它的目標對象和響應方法。

離散的、連續的手勢

手勢要么是離散的就是連續的。點擊就是離散的手勢,只發生一次。縮放則是連續的,需要一段時間。對于離散的手勢,手勢識別器向它的目標對象發送單一的操作消息。而連續的手勢,手勢識別器將會持續的發送操作消息到目標對象,直到多點觸控序列終止。 如圖1-2:

Snip20170908_5.png

通過手勢識別器響應事件

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

  • 創建和配置一個手勢識別器實例,這個步驟包括制定一個目標對象、響應方法,以及手勢的一些特有屬性(例如,需要幾根指頭)。
  • 將手勢識別器添加到視圖。
  • 實現響應方法以處理手勢。

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

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

創建手勢識別器對象之后,你需要去建立并連接響應方法。這個響應方法將會在手勢識別器識別到手勢時調用,你也可以創建并連接手勢識別器的關聯屬性。你的代碼應該參照清單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子類來創建手勢識別器,例如UIPinchGestureRecognizer。你可以參照清單1-2來指定目標對象和響應方法以初始化一個手勢識別器,在大多數情況下,目標對象會是視圖所在的視圖控制器。

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

 - (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
  }

響應離散手勢

當你創建一個手勢識別器,你會將它與一個響應方法進行連接。利用這個響應方法來響應手勢識別器對應的手勢。清單1-3展示了一個響應離散手勢的例子。當用戶點擊了添加了手勢識別器的視圖,控制器將會顯示一個圖片以示發生了點擊。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 響應向右或向左的輕掃手勢

// 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;
}];

響應連續的手勢

連續的手勢允許你的APP響應正在發生的手勢。例如,你的APP能在用戶捏合(pinch)的時候不斷的進行縮放,也可在用戶拖拽的時候圍繞著屏幕運動。

清單1-5展示了一個與手勢相同角度不斷旋轉的圖片,并且當用戶停止轉動時,讓圖片旋轉回到水平,與此同時將圖片進行動畫淡出。當用戶旋轉指頭,showGestureForRotationRecognizer:方法會被持續的調用直到所有的手指離開屏幕。

清單 1-5 響應旋轉手勢

// 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;
            }];
       }
}

每當showGestureForRotationRecognizer: 方法被調用,在drawImageForGestureRecognizer:方法中,圖片都會被設置為不透明的。當手勢結束,圖片在animateWithDuration: 方法中被設置為透明的。showGestureForRotationRecognizer:根據校對手勢識別器的狀態確定了手勢是否已經完成。手勢識別器的狀態會在稍后的文章中進行詳細的說明。

定義手勢的交互方式

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

手勢識別器在有限的狀態機下運轉

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

圖1-3 手勢識別器狀態機

Snip20170911_6.png

當一個離散的手勢識別器識別到手勢,他的狀態由可能(Possible)變為識別到的(UIGestureRecognizerStateRecognized),并且識別過程結束。

對于連續的手勢,手勢識別器的狀態在第一次識別到手勢時由可能變為開始(UIGestureRecognizerStateBegan)。然后變為改變的(UIGestureRecognizerStateChanged),并且將不斷的由改變的轉換為改變的。當用戶最后的手指離開視圖,狀態變為結束的(UIGestureRecognizerStateEnded)并且手勢識別結束。注意,結束狀態是識別到的(UIGestureRecognizerStateRecognized)別名而已。

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

每當手勢識別器的狀態改變,他都將發送動作消息到他的目標對象,除非狀態改變為失敗或者取消。因此一個離散的手勢識別器只發送單個動作消息,當狀態由可能變為識別到的。而一個連續的手勢識別器在狀態改變時會發送很多動作消息。

當手勢識別器的狀態變為識別到的(結束的)狀態,便重置狀態到可能的(Possible),此過程不發送動作消息。

與其他手勢識別器交互

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

當一個視圖添加了多點觸控手勢識別器,你可能希望改變手識別器對于觸摸事件接收和分析的競爭方式。在默認情況下,并沒有設定的順序規定哪個手勢識別器最先收到觸摸事件,因為這個原因,觸摸事件每次會以不同的順序發送給手勢識別器。你可以覆寫這一默認的行為:

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

子類通過實現UIGestureRecognizer的類方法、代理方法,和覆寫父類方法來實現這些行為。

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

想象你希望識別輕掃(Swipe)和拖動(Pan)手勢,并且你希望他們觸發不同的操作。在默認情況下,當用戶試圖進行輕掃,這個手勢會被解釋為拖動(Pan)。因為輕掃手勢在滿足被解釋為輕掃手勢(離散的手勢)的條件之前滿足了被解釋為拖動手勢(連續的手勢)的條件。

如果視圖同時能夠識別輕掃和拖動,而你希望輕掃手勢識別器在拖動手勢識別器之前分析觸摸事件。如果輕掃手勢識別器判定觸摸是輕掃,那么拖動手勢識別器永遠不需要去分析觸摸, 如果輕掃手勢識別器判定觸摸不是輕掃,他會變為失敗狀態,并且拖動手勢識別器應該開始分析觸摸事件。

你通過將想要延遲分析事件的手勢調用requireGestureRecognizerToFail:方法來聲明兩個手勢識別器的關系,參見清單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:方法向消息接收者(理解為該方法的調用者)發送消息規定只有當其他的手勢識別器識別失敗后接收者才能開始識別和分析觸摸事件。在等待其他手勢識別器狀態變為失敗的過程當中,接收者的狀態始終為可能。如果其他的手勢識別器識別失敗,接收者的狀態會發生改變。另一方面,如果其他的手勢識別器狀態變為識別到的或者開始,接收者手勢識別器狀態將變為失敗狀態。更多關于狀態變換的信息,請看"手勢識別器在有限的狀態機下運轉"。

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

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

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

當觸摸開始,如果你能夠立刻判定你的手勢識別器是否應該考慮這次觸摸,那么你可以使用gestureRecognizer:shouldReceiveTouch:方法。這個方法會在每次發生觸摸的時候被調用。一個觸摸發生時,返回NO的手勢識別器不會收到事件的通知。

清單1-7 通過gestureRecognizer:shouldReceiveTouch:代理方法阻止一個自定義視圖的單擊手勢識別器收到觸摸事件。 當觸摸發生,gestureRecognizer:shouldReceiveTouch:方法別調用。它判斷用戶是否觸摸了自定義視圖,如果是的,則阻止單擊手勢識別器收到觸摸事件。

清單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; }

如果你需要等待足夠長的時間才能決定一個手勢識別器是否應該分析一個觸摸,那么你可以使用* gestureRecognizerShouldBegin:* 代理方法。一般情況下,如果你在UIView或者UIControl的子類中自定義事件處理并且存在手勢識別器競爭,那么你可以使用該方法。返回NO將導致手勢識別器立馬識別失敗,允許其他手勢識別器繼續進行觸摸處理。如果手勢識別要阻止視圖或者控件收到觸摸事件,這個方法會在手勢識別器試圖離開可能(Possible)狀態時被調用。

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

允許同時進行手勢識別

默認情況下,兩個手勢識別器不能同時識別他們各自的手勢。但是假設你希望用戶能夠同時對視圖進行縮放和旋轉。你需要通過實現gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法來改變原本的默認行為,該方法為UIGestureRecognizerDelegateprotocol協議的可選實現方法。當一個手勢識別器器分析一個手勢而且會阻止另一個手勢識別器識別該手勢時,此方法會被調用,反之亦然。此方法默認返回NO。當你希望兩個手勢識別器同時進行手勢分析時返回YES。

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

指定兩個手勢識別器的單向關系

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

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

并且覆寫旋轉手勢識別器子類方法來返回NO。關于如何實現UIGestureRecognizer子類,參見"創建自定義手勢識別器"(后序文章)。

如果兩個手勢并不存在互相的阻止,使用gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,參見"允許同時進行手勢識別"。默認情況下,一個捏合手勢阻止一個旋轉手勢,反之亦然,因為兩個手勢不能被同時識別。

與用戶界面控件交互

iOS6.0及其以后,默認的控件操作會防止重疊的手勢識別器行為。例如,按鈕的默認操作是單擊。如果你向按鈕的父視圖添加了單擊手勢識別器,并點擊了按鈕,按鈕的響應方法會收到觸摸事件而不是手勢識別器。這種情況出現在手勢識別器與默認的控件操作重疊時,例如:

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

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

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

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

一個事件包含了當前多點觸控序列的所有觸摸

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

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

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

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

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

Snip20170912_10.png

注意:一個手指并沒有鼠標精確。當用戶觸摸屏幕,接觸的區域其實是橢圓形的而且比我們想象中的位置偏低。這個接觸區域的變化取決于手指的尺寸、方向、壓力、哪一根手指等因素。底層的多點觸控系統幫你分析和計算了觸摸點,因此你不需要自己編寫代碼來實現這些。

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

在多點觸控序列過程中,如果在特定的觸摸階段產生了新的觸摸或者觸摸發生了改變,APP將會發送觸摸消息。會調用如下方法:

上面的每一個方法都與一個觸摸階段相關聯;例如,touchesBegan:withEvent:方法和UITouchPhaseBegan相關聯。觸摸階段保存在觸摸對象的phase屬性當中。

注意:這些方法并不和手勢識別器的狀態相關聯,例如UIGestureRecognizerStateBeganUIGestureRecognizerStateEnded。手勢識別器的狀態充分的表明了其本身的階段,而不是識別到的觸摸對象的階段。

調節發送到視圖的觸摸

或許在某些情況下,你希望一個視圖在手勢識別器之前收到觸摸。但是,在你能夠改變觸摸傳遞到視圖的路徑之前,你需要理解默認的行為。在這個簡單的例子中,當一個觸摸發生,觸摸對象從UIApplication對象傳遞到UIWindow對象(我的備注:實際上傳遞的是UIEvent對象)。然后,窗口(window)對象首先將觸摸發送到被添加到觸摸點發生處的視圖(或者它的父視圖)上的手勢識別器,在發送給視圖本身之前。

圖1-5 默認的觸摸事件傳遞步驟

Snip20170912_11.png

手勢識別器首先識別觸摸

一個窗口(window)對象延遲發送觸摸對象給視圖,為了手勢識別器能首先分析觸摸。在延遲過程中,如果手勢識別器識別到了觸摸手勢,那么窗口(window)對象永遠不會將觸摸對象發送給視圖,并且會取消任何之前發送給視圖的觸摸對象---識別的序列的一部分。

例如,你有一個需要兩個手指觸摸的離散手勢識別器,觸摸會被轉換成兩個不同的觸摸對象。當觸摸發生,觸摸點視圖的觸摸對象從APP對象發送到window對象,并且接下來發生的序列,如圖1-6描述:

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

Snip20170912_12.png
  1. 窗口(window)對象在開始階段發送兩個觸摸對象---通過touchesBegan:withEvent:方法發送給手勢識別器。 手勢識別器還沒有完成手勢的識別,因此它的狀態仍是可能(Possible)。窗口對象會將同樣的觸摸對象發送給手勢識別器相關聯的視圖。
  2. 窗口(window)對象在移動階段發送兩個觸摸對象---通過touchesMoved:withEvent:方法發送給手勢識別器。 手勢識別器還沒有完成手勢的識別,因此它的狀態仍是可能(Possible)。窗口對象會將同樣的觸摸對象發送給手勢識別器相關聯的視圖。
  3. 窗口(window)對象在結束階段發送一個觸摸對象---通過touchesEnded:withEvent:方法發送給手勢識別器。 這個觸摸對象沒有包含足夠的手勢信息,但是窗口(window)對象不發送該對象到視圖。
  4. 窗口(window)對象在結束階段發送另一個觸摸對象。此時手勢識別器識別到了手勢,因此其狀態變為識別到的。在第一個操作消息發送前,視圖調用touchesCancelled:withEvent:方法去取消之前在開始和移動階段發送的觸摸對象。觸摸在結束階段被取消掉(注意:touchesEnded:withEvent:并不會被調用)。

現在假設手勢識別器在最后一步判定多點觸控序列的分析結果并非自己的手勢。它將狀態設置為UIGestureRecognizerStateFailed。然后窗口對象通過* touchesEnded:withEvent:*消息體發送兩個觸摸對象給關聯的視圖。

一個連續的手勢識別器遵循相似的序列,除非它可能在結束階段前識別到手勢。快要識別到手勢前,他的狀態變為UIGestureRecognizerStateBegan(而不是識別到的)。窗口對象(window)發送隨后的多點觸控序列中的觸摸對象給手勢識別器而不是關聯的視圖。

改變觸摸到視圖的發送

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

  • delaysTouchesBegan(默認為NO)---正常情況下,窗口對象會發送開始和移動階段的觸摸對象給視圖和手勢識別器。設置delaysTouchesBegan為YES阻止窗口發送開始和移動階段的觸摸對象給視圖。這樣能夠保證當一個手勢識別器識別手勢時,不會有任何觸摸對象發送給關聯的視圖。慎重使用該屬性,因為它會導致你的用戶界面看起來反應遲鈍。

  • delaysTouchesEnded(默認為YES)---當這個屬性被設置為YES,它確保視圖不會完成一個手勢想要取消的動作。當一個手勢識別器正在識別觸摸事件,窗口對象不會將結束階段的觸摸對象發送給給關聯的視圖。如果一個手勢識別器識別到了手勢,這個觸摸對象將會被取消。如果手勢識別器沒有識別到手勢,窗口對象會通過delaysTouchesEnded消息發送觸摸對象到關聯的視圖。設置該屬性值為NO以允許視圖和手勢識別器同時分析結束階段的觸摸對象。

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

一個手勢識別器監測一個觸摸并判斷他是否為手勢的一部分,他能直接將觸摸傳遞給視圖。要做到這樣,手勢識別器自己調用ignoreTouch:forEvent:,傳遞觸摸對象。

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

后序研究

cancelsTouchesInView屬性默認值為YES,當手勢識別器識別到手勢時會調用。
touchesCancelled:withEvent:方法,而touchesEnded:withEvent:不會調用,只有當識別失敗才會調用。
如果cancelsTouchesInView被設置為NO,設置delaysTouchesEnded并無作用。

創建自定義手勢識別器

為了實現自定義的手勢識別器,首先需要創建一個UIGestureRecognizer的子類。然后在子類的頭文件中添加如下導入命令:

 #import <UIKit/UIGestureRecognizerSubclass.h>

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

- (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在觸摸處理方法中接收觸摸"(在前面內容中)中描述的方法具備相同的方法簽名和行為。所有你在子類中覆寫的方法都必須調用父類實現,盡管父類只有方法的空實現。

注意UIGestureRecognizerSubclass.h中的state屬性現在是可讀可寫的而不僅僅只是可讀,你在子類中使用UIGestureRecognizerState枚舉來設置state屬性值。

實現自定義手勢識別器的觸摸事件處理方法

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

這個例子只有一個視圖,但是大多數APP擁有許多視圖。一般情況下,你需要將觸摸的位置轉換為相對于屏幕的坐標,你才能正確的識別拖動視圖的手勢。

清單1-8 實現勾選手勢識別器

#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;
}

離散手勢和連續手勢的狀態切換時不同的,在前面的內容中"手勢識別器在有限狀態機下運轉"有描述。當你創建一個自定義的手勢識別器,你通過設置相關的狀態來聲明它是離散的還是連續的。作為示例,清單1-8的勾選手勢識別器沒有將狀態設置為開始的(Begin)或者改變的(Changed),因為它是離散的。

當你實現一個子類的手勢識別器是,最重要的事情是你需要正確的設置手勢識別器的狀態(state)。iOS系統需要知道手勢識別器的狀態以讓他按照預期那樣交互。例如,如果你需要同時進行手勢識別或者需要某個手勢識別器失敗,iOS需要明白你當前手勢的狀態。

關于更多自定義手勢識別器,請參見WWDC 2012: Building Advanced Gesture Recognizers

重置手勢識別器的狀態

如果你的手勢識別器變為識別到的、結束的、失敗的、或者取消的,UIGestureRecognizer會在返回到可能(Possible)狀態前調用reset方法。

清單1-9,實現了reset方法來重置手勢識別器內部的各種狀態以準備好識別新的手勢。手勢識別器返回了這個方法后,他將不會收到進程中觸摸對象進一步更新。

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

- (void)reset {
    [super reset];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 好奇觸摸事件是如何從屏幕轉移到APP內的?困惑于Cell怎么突然不能點擊了?糾結于如何實現這個奇葩響應需求?亦或是...
    Lotheve閱讀 57,915評論 51 603
  • 手勢識別器是附加到視圖的對象,將低級別事件處理代碼轉換為更高級別的操作,它允許視圖以控件執行的方式響應操作。 手勢...
    坤坤同學閱讀 4,123評論 0 9
  • 在iOS開發中經常會涉及到觸摸事件。本想自己總結一下,但是遇到了這篇文章,感覺總結的已經很到位,特此轉載。作者:L...
    WQ_UESTC閱讀 6,107評論 4 26
  • 在開發過程中,大家或多或少的都會碰到令人頭疼的手勢沖突問題,正好前兩天碰到一個類似的bug,于是借著這個機會了解了...
    閆仕偉閱讀 5,403評論 2 23
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點觸摸的強大功能,喬布斯讓人們認識到手機其實...
    翹楚iOS9閱讀 3,003評論 0 13