iOS:事件處理機制

  官方文檔說明:《Event Handling Guide for iOS》,本文參考轉載文章,並參照官方文檔補充說明。html

  本篇內容將圍繞iOS中事件及其傳遞機制進行學習和分析。在iOS中,事件分爲三類:ios

  • 觸控事件(單點、多點觸控以及各類手勢操做)
  • 傳感器事件(重力、加速度傳感器等)
  • 遠程控制事件(遠程遙控iOS設備多媒體播放等)

  這三類事件共同構成了iOS設備豐富的操做方式和使用體驗,本次就首先來針對第一類事件:觸控事件,進行學習和分析。設計模式

     Gesture Recognizers

  Gesture Recognizers是一類手勢識別器對象,它能夠附屬在你指定的View上,而且爲其設定指定的手勢操做,例如是點擊、滑動或者是拖拽。當觸控事件 發生時,設置了Gesture Recognizers的View會先經過識別器去攔截觸控事件,若是該觸控事件是事先爲View設定的觸控監聽事件,那麼Gesture Recognizers將會發送動做消息給目標處理對象,目標處理對象則對此次觸控事件進行處理,先看看以下流程圖。app

  在iOS中,View就是咱們在屏幕上看到的各類UI控件,當一個觸控事件發生時,Gesture Recognizers會先獲取到指定的事件,而後發送動做消息(action message)給目標對象(target),目標對象就是ViewController,在ViewController中經過事件方法完成對該事件的處理。Gesture Recognizers能設置諸如單擊、滑動、拖拽等事件,經過Action-Target這種設計模式,好處是能動態爲View添加各類事件監聽,而不用去實現一個View的子類去完成這些功能。框架

  以上過程就是咱們在開發中在方法中常見的設置action和設置target,例如爲UIButton設置監聽事件等。ide

            經常使用手勢識別類

  在UIKit框架中,系統爲咱們事先定義好了一些經常使用的手勢識別器,包括點擊、雙指縮放、拖拽、滑動、旋轉以及長按。經過這些手勢識別器咱們能夠構造豐富的操做方式。佈局

  在上表中能夠看到,UIKit框架中已經提供了諸如UITapGestureRecognizer在內的六種手勢識別器,若是你須要實現自定義的手勢識別器,也能夠經過繼承UIGestureRecognizer類並重寫其中的方法來完成,這裏咱們就不詳細討論了。學習

  每個Gesture Recognizer關聯一個View,可是一個View能夠關聯多個Gesture Recognizer,由於一個View可能還能響應多種觸控操做方式。爲了使gesture recognizer識別發生在view上面的手勢,你必須attach gesture recognizer to that view。當一個觸控事件發生時,Gesture Recognizer接收一個touch發生的消息要先於View自己,結果就是Gesture Recognizer能夠表明view迴應視圖上的touches事件。ui

  當gesture recognizer識別了一個特定手勢,它就會發送一條動做消息(action message)給它的target,因此建立gesture recognizer時候,須要initialize it with a targer and action。

連續和不連續動做

  觸控動做同時分爲連續動做(continuous)和不連續動做(discrete),連續動做例如滑動和拖拽,它會持續一小段時間,而不連續動做例如單擊,它瞬間就會完成,在這兩類事件的處理上又稍有不一樣。對於不連續動做,Gesture Recognizer只會給ViewContoller發送一個單一的動做消息(action message),而對於連續動做,Gesture Recognizer會發送多條動做消息給ViewController,直到全部的事件都結束。 spa

         添加GestureRecognizer                  

  爲一個View添加GestureRecognizer有兩種方式,一種是經過InterfaceBuilder實現,另外一種就是經過代碼實現,咱們看看經過代碼來如何實現。

 1 - (void)viewDidLoad {
 2      [super viewDidLoad];
 3 
 4      // 建立並初始化手勢對象
 5      UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
 6           initWithTarget:self action:@selector(respondToTapGesture:)];
 7 
 8      // 指定操做爲單擊一次
 9      tapRecognizer.numberOfTapsRequired = 1;
10 
11      // 爲當前View添加GestureRecognizer
12      [self.view addGestureRecognizer:tapRecognizer];
13 
14      // ...
15 }

  經過上述代碼,咱們實現了爲當前MyViewController的View添加一個單擊事件,首先構造了UITapGestureRecognizer對象,指定了target爲當前ViewController自己,action就是後面本身實現的處理方法,這裏就呼應了前文提到的Action-Target模式。

  在事件處理過程當中,這兩種方式所處的狀態又各有不一樣,首先,全部的觸控事件最開始都是處於可用狀態(Possible),對應UIKit裏面的UIGestureRecognizerStatePossible類,若是是不連續動做事件,則狀態只會從Possible轉變爲已識別狀態(Recognized,UIGestureRecognizerStateRecognized)或者是失敗狀態(Failed,UIGestureRecognizerStateFailed)。例如一次成功的單擊動做,就對應了Possible-Recognized這個過程。

  若是是連續動做事件,若是事件沒有失敗而且連續動做的第一個動做被成功識別(Recognized),則從Possible狀態轉移到Began(UIGestureRecognizerStateBegan)狀態,這裏表示連續動做的開始,接着會轉變爲Changed(UIGestureRecognizerStateChanged)狀態,在這個狀態下會不斷循環的處理連續動做,直到動做執行完成變轉變爲Recognized已識別狀態,最終該動做會處於完成狀態(UIGestureRecognizerStateEnded),另外,連續動做事件的處理狀態會從Changed狀態轉變爲Canceled(UIGestureRecognizerStateCancelled)狀態,緣由是識別器認爲當前的動做已經不匹配當初對事件的設定了。每一個動做狀態的變化,Gesture Recognizer都會發送消息(action message)給Target,也就是ViewController,它能夠根據這些動做消息進行相應的處理。例如一次成功的滑動手勢動做就包括按下、移動、擡起的過程,分別對應了Possible-Began-Changed-Recognized這個過程。

 

UITouch & UIEvent

  在屏幕上的每一次動做事件都是一次Touch,在iOS中用UITouch對象表示每一次的觸控,多個Touch組成一次Event,用UIEvent來表示一次事件對象。

  在上述過程當中,完成了一次雙指縮放的事件動做,每一次手指狀態的變化都對應事件動做處理過程當中得一個階段。經過Began-Moved-Ended這幾個階段的動做(Touch)共同構成了一次事件(Event)。在事件響應對象UIResponder中有對應的方法來分別處理這幾個階段的事件。

  • touchesBegan:withEvent:
  • touchesMoved:withEvent:
  • touchesEnded:withEvent:
  • touchesCancelled:withEvent:

  後面的參數分別對應UITouchPhaseBegan、UITouchPhaseMoved、UITouchPhaseEnded、UITouchPhaseCancelled這幾個類。用來表示不一樣階段的狀態。

事件傳遞

  如上圖,iOS中事件傳遞首先從App(UIApplication)開始,接着傳遞到Window(UIWindow),在接着往下傳遞到View以前,Window會將事件交給GestureRecognizer,若是在此期間,GestureRecognizer識別了傳遞過來的事件,則該事件將不會繼續傳遞到View去,而是像咱們以前說的那樣交給Target(ViewController)進行處理。

響應者鏈(Responder Chain)

  一般,一個iOS應用中,在一塊屏幕上一般有不少的UI控件,也就是有不少的View,那麼當一個事件發生時,如何來肯定是哪一個View響應了這個事件呢,接下來咱們就一塊兒來看看。

尋找hit-test view

  什麼是hit-test view呢?簡單來講就是你觸發事件所在的那個View,尋找hit-test view的過程就叫作Hit-Testing。那麼,系統是如何來執行Hit-Testing呢,首先假設如今有以下這麼一個UI佈局,一種有ABCDE五個View。

  假設一個單擊事件發生在了View D裏面,系統首先會從最頂層的View A開始尋找,發現事件是在View A或者其子類裏面,那麼接着從B和C找,發現事件是在C或者其子類裏面,那麼接着到C裏面找,這時發現事件是在D裏面,而且D已經沒有子類了,那麼hit-test view就是View D啦。

響應者對象(Responsder Object)

  響應者對象是可以響應而且處理事件的對象,UIResponder是全部響應者對象的父類,包括UIApplication、UIView和UIViewController都是UIResponder的子類。也就意味着全部的View和ViewController都是響應者對象。

第一響應者(First Responder)

  第一響應者是第一個接收事件的View對象,咱們在Xcode的Interface Builder畫視圖時,能夠看到視圖結構中就有First Responder。

  這裏的First Responder就是UIApplication了。另外,咱們能夠控制一個View讓其成爲First Responder,經過實現 canBecomeFirstResponder方法並返回YES可使當前View成爲第一響應者,或者調用View的becomeFirstResponder方法也能夠,例如當UITextField調用該方法時會彈出鍵盤進行輸入,此時輸入框控件就是第一響應者。

事件傳遞機制

  如上所說,,若是hit-test view不能處理當前事件,那麼事件將會沿着響應者鏈(Responder Chain)進行傳遞,知道遇到能處理該事件的響應者(Responsder Object)。經過下圖,咱們來看看兩種不一樣狀況下得事件傳遞機制。

  左邊的狀況,接收事件的initial view若是不能處理該事件而且她不是頂層的View,則事件會往它的父View進行傳遞。initial view的父View獲取事件後若是仍不能處理,則繼續往上傳遞,循環這個過程。若是頂層的View仍是不能處理這個事件的話,則會將事件傳遞給它們的ViewController,若是ViewController也不能處理,則傳遞給Window(UIWindow),此時Window不能處理的話就將事件傳遞給Application(UIApplication),最後若是連Application也不能處理,則廢棄該事件。

  右邊圖的流程惟一不一樣就在於,若是當前的ViewController是由層級關係的,那麼當子ViewController不能處理事件時,它會將事件繼續往上傳遞,直到傳遞到其Root ViewController,後面的流程就跟以前分析的同樣了。

  這就是事件響應者鏈的傳遞機制,經過這些內容,咱們能夠更深刻的瞭解事件在iOS中得傳遞機制,對咱們在實際開發中更好的理解事件操做的原理有很大的幫助,也對咱們實現複雜佈局進行事件處理時增添了多一份的理解。

事件處理方法

UIResponder中定義了一系列對事件的處理方法,他們分別是:

  • –(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

  從方法名字能夠知道,他們分別對應了屏幕事件的開始、移動、結束和取消幾個階段,前三個階段理解都沒問題,最後一個取消事件的觸發時機是在諸如忽然來電話或是系統殺進程時調用。這些方法的第一個參數定義了UITouch對象的一個集合(NSSet),它的數量表示了此次事件是幾個手指的操做,目前iOS設備支持的多點操做手指數最可能是5。第二個參數是當前的UIEvent對象。下圖展現了一個UIEvent對象與多個UITouch對象之間的關係。

1、點擊事件

  首先,新建一個自定義的View繼承於UIView,並實現上述提到的事件處理方法,咱們能夠經過判斷UITouch的tapCount屬性來決定響應單擊、雙擊或是屢次點擊事件。

 1 #import "MyView.h"
 2 @implementation MyView
 3 
 4 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
 5 {
 6 
 7 }
 8 
 9 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
10 {
11 
12 }
13 
14 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
15 {
16     for (UITouch *aTouch in touches) {
17         if (aTouch.tapCount == 2) {
18             // 處理雙擊事件
19             [self respondToDoubleTapGesture];
20         }
21     }
22 }
23 
24 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
25 {
26 
27 }
28 
29 -(void)respondToDoubleTapGesture
30 {
31     NSLog(@"respondToDoubleTapGesture");
32 }
33 
34 @end

2、滑動事件

  滑動事件通常包括上下滑動和左右滑動,判斷是不是一次成功的滑動事件須要考慮一些問題,好比大部分狀況下,用戶進行一次滑動操做,此次滑動是不是在一條直線上?或者是不是基本能保持一條直線的滑動軌跡。或者判斷是上下滑動仍是左右滑動等。另外,滑動手勢通常有一個起點和一個終點,期間是在屏幕上畫出的一個軌跡,因此須要對這兩個點進行判斷。咱們修改上述的MyView.m的代碼來實現一次左右滑動的事件響應操做。

 1 #import "MyView.h"
 2 
 3 #define HORIZ_SWIPE_DRAG_MIN  12    //水平滑動最小間距
 4 #define VERT_SWIPE_DRAG_MAX    4    //垂直方向最大偏移量
 5 
 6 @implementation MyView
 7 
 8 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
 9 {
10     UITouch *aTouch = [touches anyObject];
11     // startTouchPosition是一個CGPoint類型的屬性,用來存儲當前touch事件的位置
12     self.startTouchPosition = [aTouch locationInView:self];
13 }
14 
15 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
16 {
17 
18 }
19 
20 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
21 {
22     UITouch *aTouch = [touches anyObject];
23     CGPoint currentTouchPosition = [aTouch locationInView:self];
24 
25     //  判斷水平滑動的距離是否達到了設置的最小距離,而且是不是在接近直線的路線上滑動(y軸偏移量)
26     if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
27         fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
28     {
29         // 知足if條件則認爲是一次成功的滑動事件,根據x座標變化判斷是左滑仍是右滑
30         if (self.startTouchPosition.x < currentTouchPosition.x) {
31             [self rightSwipe];//右滑響應方法
32         } else {
33             [self leftSwipe];//左滑響應方法
34         }
35         //重置開始點座標值
36         self.startTouchPosition = CGPointZero;
37     }
38 }
39 
40 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
41 {
42   //當事件因某些緣由取消時,重置開始點座標值
43     self.startTouchPosition = CGPointZero;
44 }
45 
46 -(void)rightSwipe
47 {
48     NSLog(@"rightSwipe");
49 }
50 
51 -(void)leftSwipe
52 {
53     NSLog(@"leftSwipe");
54 }
55 
56 @end

3、拖拽事件

  在屏幕上咱們能夠拖動某一個控件(View)進行移動,這種事件成爲拖拽事件,其實現原理就是在不改變View的大小尺寸的前提下改變View的顯示座標值,爲了達到動態移動的效果,咱們能夠在move階段的方法中進行座標值的動態更改,仍是重寫MyView.m的事件處理方法,此次在touchesMove方法中進行處理。

 1 #import "MyView.h"
 2 @implementation MyView
 3 
 4 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
 5 {
 6 
 7 }
 8 
 9 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
10 {
11     UITouch *aTouch = [touches anyObject];
12     //獲取當前觸摸操做的位置座標
13     CGPoint loc = [aTouch locationInView:self];
14     //獲取上一個觸摸點的位置座標
15     CGPoint prevloc = [aTouch previousLocationInView:self];
16 
17     CGRect myFrame = self.frame;
18     //改變View的x、y座標值
19     float deltaX = loc.x - prevloc.x;
20     float deltaY = loc.y - prevloc.y;
21     myFrame.origin.x += deltaX;
22     myFrame.origin.y += deltaY;
23     //從新設置View的顯示位置
24     [self setFrame:myFrame];
25 }
26 
27 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
28 {
29 
30 }
31 
32 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
33 {
34 
35 }
36 
37 @end

4、雙指縮放

  以前提到過UIEvent包含了一系列的UITouch對象構成一次事件,當設計多點觸控操做時,可與對UIEvent對象內的UITouch對象進行處理,好比實現一個雙指縮放的功能。

 1 #import "MyView.h"
 2 @implementation MyView
 3 {
 4     BOOL pinchZoom;
 5     CGFloat previousDistance;
 6     CGFloat zoomFactor;
 7 }
 8 
 9 -(id)init
10 {
11     self = [super init];
12     if (self) {
13         pinchZoom = NO;
14         //縮放前兩個觸摸點間的距離
15         previousDistance = 0.0f;
16         zoomFactor = 1.0f;
17     }
18     return self;
19 }
20 
21 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
22 {
23     if(event.allTouches.count == 2) {
24         pinchZoom = YES;
25         NSArray *touches = [event.allTouches allObjects];
26         //接收兩個手指的觸摸操做
27         CGPoint pointOne = [[touches objectAtIndex:0] locationInView:self];
28         CGPoint pointTwo = [[touches objectAtIndex:1] locationInView:self];
29         //計算出縮放先後兩個手指間的距離絕對值(勾股定理)
30         previousDistance = sqrt(pow(pointOne.x - pointTwo.x, 2.0f) +
31                                 pow(pointOne.y - pointTwo.y, 2.0f));
32     } else {
33         pinchZoom = NO;
34     }
35 }
36 
37 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
38 {
39     if(YES == pinchZoom && event.allTouches.count == 2) {
40         NSArray *touches = [event.allTouches allObjects];
41         CGPoint pointOne = [[touches objectAtIndex:0] locationInView:self];
42         CGPoint pointTwo = [[touches objectAtIndex:1] locationInView:self];
43         //兩個手指移動過程當中,兩點之間距離
44         CGFloat distance = sqrt(pow(pointOne.x - pointTwo.x, 2.0f) +
45                                 pow(pointOne.y - pointTwo.y, 2.0f));
46         //換算出縮放比例
47         zoomFactor += (distance - previousDistance) / previousDistance;
48         zoomFactor = fabs(zoomFactor);
49         previousDistance = distance;
50 
51         //縮放
52         self.layer.transform = CATransform3DMakeScale(zoomFactor, zoomFactor, 1.0f);
53     }
54 }
55 
56 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
57 {
58     if(event.allTouches.count != 2) {
59         pinchZoom = NO;
60         previousDistance = 0.0f;
61     }
62 }
63 
64 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
65 {
66 
67 }
68 
69 @end

  上面實現的方式有一點不足之處就是必須兩個手指同時觸摸按下才能達到縮放的效果,並不能達到相冊裏面那樣一個手指觸摸後,另外一個手指按下也能夠縮放。若是須要達到和相冊照片縮放的效果,須要同時控制begin、move、end幾個階段的事件處理。這個不足就留給感興趣的同窗本身去實現了。

 

 

參考文章:

 http://ryantang.me/blog/2013/12/07/ios-event-dispatch-1/ 

 http://ryantang.me/blog/2013/12/29/ios-event-dispatch-2/ 

相關文章
相關標籤/搜索