iOS觸摸事件處理詳解

簡介

iOS 事件分為三大類

  • 觸摸事件
  • 加速器事件
  • 遠程控制事件

以下我們講解觸摸事件
觸摸事件是我們平時遇到最多的事件,例如單擊、長按、滑動等等。當(dāng)用戶點擊按鈕,到按鈕處理回調(diào)。整個過程是如何發(fā)生,需要什么樣的原則,這些都是問題。為了使系統(tǒng)能更加鮮明符合用戶的操作邏輯,iOS系統(tǒng)將事件相應(yīng)過程拆分成兩部分:1.尋找響應(yīng)鏈;2.事件響應(yīng)。先將事件通過某種規(guī)則來分發(fā),找到處理事件的控件。其次是將事件傳遞分發(fā),響應(yīng)。

觸摸事件

UIEvent
iOS將觸摸事件定義為第一個手指開始觸摸屏幕到最后一個手指離開屏幕定義為一個觸摸事件。用類UIEvent表示。

UITouch
一個手指第一次點擊屏,會形成一個UITouch對象,直到離開銷毀。表示觸碰。UITouch對象能表明了當(dāng)前手指觸碰的屏幕位置,狀態(tài)。狀態(tài)分為開始觸碰、移動、離開。

根據(jù)定義,UIEvent實際包括了多個UITouch對象。有幾個手指觸碰,就會有幾個UITouch對象。
代碼定義如下

@interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval  timestamp;
#if UIKIT_DEFINE_AS_PROPERTIES
//UITouch SET
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
//省略部分代碼
@end

UIEventType表明了事件類型,UIEvent表示了三大事件。
allTouches是該事件的所有UITouch對象的集合。

//UITouch
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval      timestamp;
@property(nonatomic,readonly) UITouchPhase        phase;
@property(nonatomic,readonly) NSUInteger          tapCount;   // touch down within a certain point within a certain amount of time
@property(nonatomic,readonly) UITouchType         type NS_AVAILABLE_IOS(9_0);

@property(ullable,nonatomic,readonly,strong) UIWindow                        *window;
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers 
NS_AVAILABLE_IOS(3_2);
//省略部分代碼
@end
//Touch 狀態(tài)枚舉
typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,             // whenever a finger touches the surface.
    UITouchPhaseMoved,             // whenever a finger moves on the surface.
    UITouchPhaseStationary,        // whenever a finger is touching the surface but hasn't moved since the previous event.
    UITouchPhaseEnded,             // whenever a finger leaves the surface.
    UITouchPhaseCancelled,         // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};

UITouch中phase表明了手指移動的狀態(tài),包括1.開始點擊;2.移動;3.保持; 4.離開;5.被取消(手指沒有離開屏幕,但是系統(tǒng)不再跟蹤它了)

綜上,UIEvent就是一組UITouch。每當(dāng)該組中任何一個UITouch對象的phase發(fā)生變化,系統(tǒng)都會產(chǎn)生一條TouchMessage。也就是說每次用戶手指的移動和變化,UITouch都會形成狀態(tài)改變,系統(tǒng)變回會形成Touch message進行傳遞和派發(fā)。那么 一次觸摸事件是由一組UITouch對象狀態(tài)變化引起的一組Touch message的轉(zhuǎn)發(fā)和派送。那么事件派發(fā)的原則是什么?

響應(yīng)鏈

響應(yīng)鏈?zhǔn)恰笆录砂l(fā)”的原則和規(guī)定,那么響應(yīng)鏈?zhǔn)鞘裁矗款櫭剂x事件鏈?zhǔn)且粋€鏈條,詳細的定義如下:

  • 每條鏈?zhǔn)且粋€ 鏈表狀結(jié)構(gòu),整個是一棵樹
  • 鏈表的每一個node是一個 UIResponser對象

先看下UIResponser,UIResponser是用來做什么的?

UIResponser就是用來接收和處理事件的類,先拋開iOS中的具體傳遞細節(jié),系統(tǒng)發(fā)送UIEvent的Touch message給UIResponser類。UIResponser提供了一下幾個函數(shù)來做事件處理

//觸摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

//物理按鈕,遙控器上面的按鈕在按壓狀態(tài)等狀態(tài)下的回調(diào)
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

//設(shè)備的陀螺儀和加速傳感器使用
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

UIResponser包括了各種Touch message 的處理,比如開始,移動,停止等等。常見的UIResponser有 UIView及子類,UIViController,APPDelegate,UIApplication等等。

回到響應(yīng)鏈,響應(yīng)鏈?zhǔn)怯蒛IResponser組成的,那么是按照哪種規(guī)則形成的。

  1. 程序啟動
  • UIApplication會生成一個單例,并會關(guān)聯(lián)一個APPDelegate。APPDelegate作為整個響應(yīng)鏈的根建立起來,而UIApplication會將自己與這個單例鏈接,即UIApplication的nextResponser(下一個事件處理者)為APPDelegate。
  1. 創(chuàng)建UIWindow
  • 程序啟動后,任何的UIWindow被創(chuàng)建時,UIWindow內(nèi)部都會把nextResponser設(shè)置為UIApplication單例。
  • UIWindow初始化rootViewController, rootViewController的nextResponser會設(shè)置為UIWindow
  1. UIViewController初始化
  • loadView, VC的view的nextResponser會被設(shè)置為VC.
  1. addSubView
  • addSubView操作過程中,如果子subView不是VC的View,那么subView的nextResponser會被設(shè)置為superView。如果是VC的View,那就是 subView -> subView.VC ->superView

如果在中途,subView.VC被釋放,就會變成subView.nextResponser = superView

最終形成類似這樣一張圖


responser chain@2x.png

其中應(yīng)該是由箭頭的,箭頭的方向是超上,也就是subView指向superView.

事件傳遞

有了響應(yīng)網(wǎng)為基礎(chǔ),事件的傳遞就比較簡單,只需要選擇其中一條響應(yīng)鏈,但是選擇那一條響應(yīng)鏈來傳遞呢?為了弄清真?zhèn)€過程,我們先來查看一下從觸摸硬件事件轉(zhuǎn)化為UIEvent消息。

  1. 首先用戶觸摸屏幕,系統(tǒng)的硬件進程會獲取到這個點擊事件,將事件簡單處理封裝后存到系統(tǒng)中,由于硬件檢測進程和當(dāng)前運行的APP是兩個進程,所以進程兩者之間傳遞事件用的是端口通信。硬件檢測進程會將事件放入到APP檢測的那個端口。
  2. 其次,APP啟動主線程RunLoop會注冊一個端口事件,來檢測觸摸事件的發(fā)生。當(dāng)時事件到達,系統(tǒng)會喚起當(dāng)前APP主線程的Runloop。喚起原因就是端口觸摸事件,主線程會分析這個事件。
  3. 最后,系統(tǒng)判斷該次觸摸是否導(dǎo)致了一個新的事件, 也就是說是否是第一個手指開始觸碰,如果是,系統(tǒng)會先從響應(yīng)網(wǎng)中 尋找響應(yīng)鏈。如果不是,說明該事件是當(dāng)前正在進行中的事件產(chǎn)生的一個Touch message, 也就是說已經(jīng)有保存好的響應(yīng)鏈。

如果是新事件,系統(tǒng)會尋找響應(yīng)鏈,為了符合用戶的操作習(xí)慣,系統(tǒng)會根據(jù)用戶的點擊位置,在當(dāng)前的整個APP的顯示層級中尋找。過程如下:

  1. 將所有的顯示在屏幕上的 "合格的"UIWindow對象 按照層級結(jié)構(gòu)從上到下排列成一個數(shù)組。
  2. 從第一個UIWindow對象開始,先判斷UIWindow是否合格,其次判斷 點擊位置在不在這個Window內(nèi),如果不在 ,返回nil, 就換下一個UIWindow;如果在的話,并且UIWindow沒有subView就返回自己,整個過程結(jié)束。如果UIWindow有subViews,就從后往前遍歷整個subViews,做和UIWindow類似的事情,直到找到一個View。如果沒有找到到就不做傳遞。

合格的UIWindow,UIView。意思是控件被允許接受事件。符合三個條件:1.不能被隱藏;2.alpha值大于0.01(不是backgroundColor為clearColor);3.isUserInteractionEnabled為YES,打開狀態(tài)。一般UILabel,UIImageView純顯示的控件默認是關(guān)閉狀態(tài),也就是不處理事件。

顯示控件有了兩個方法來做上面這件事,就是常說的hitTest

 // 先判斷點是否在View內(nèi)部,然后遍歷subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
//判斷點是否在這個View內(nèi)部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

整個過程的系統(tǒng)實現(xiàn)大致如下

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
  //判斷是否合格
    if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
        //判斷點擊位置是否在自己區(qū)域內(nèi)部
        if ([self pointInside: point withEvent:event]) {
            UIView *attachedView;
            for (int i = self.subviews.count - 1; i >= 0; i--) {
                UIView *view  = self.subviews[i];
                //對子view進行hitTest
                attachedView =  [view hitTest:point withEvent:event];
                if (attachedView)
                    break;
            }
            if (attachedView)  {
                return attachedView;
            } else {
                return self;
            }
        }
    }
    return nil;
}

技巧

以上可知默認情況下,用戶點擊哪個View,系統(tǒng)就會在尋找過程中返回哪個view,但是我們可以重載上面兩個方法做如下事情:

  • 將控件外部點規(guī)整到控件內(nèi)部。 例如控件較小,點擊位置在控件邊緣外部,可以重載- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; 將外部的點也判斷為內(nèi)部點,這樣hitTest就會遍歷自己。
  • 重載HitTest更改默認行為。 有時候點擊subView的某些特殊位置需要superView處理,我們可以在superView的hitTest,返回superView。這樣superView變成首部響應(yīng)者

hitTest的邏輯代碼中會把隱藏,透明(alpha<0.01,不是backgroundColor為clearColor),不交互的view濾過,但不代表hitTest不會被調(diào)用,我們可以重載hitTest去讓 已經(jīng)隱藏、透明、不交互的view響應(yīng)事件。不過最正規(guī)的方法是打開控件交互屬性。

** 以上過程返回的View被稱作hitTestView,順著hitTestView的nextResponser,可以形成一個鏈,即響應(yīng)鏈。** 最后指向appDelegate. 并且返回hitTestView之后,系統(tǒng)會持有hitTestView。事件不結(jié)束,這個hitTestView不會發(fā)生變化,即使用戶點擊之后將手指移動到其他控件上面,該點擊都會綁定開始的hitTestView。當(dāng)所有手指離開屏幕,事件結(jié)束。再次點擊,事件重新開始。以上過程再來一次。

事件響應(yīng)

形成響應(yīng)鏈之后,UIWindow會把事件目標(biāo)鎖定為hitTestView(響應(yīng)鏈頭的控件),當(dāng)手指狀態(tài)發(fā)生變化, 會不停的發(fā)送UITouch Message 給這個hitTestView。 下面這幾個方法會被調(diào)用。

然后控件的以下方法會陸續(xù)被調(diào)用

//點擊剛開始,回調(diào)這個方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//點擊之后移動,回調(diào)這個方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指移開,點擊結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//點擊過程中,事件被手勢識別,會回調(diào)這個方法,關(guān)于手勢后面會講解
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

技巧

由于系統(tǒng)只會把事件發(fā)送給 hitTestView,如果你想讓hitTestView之后的其他響應(yīng)者處理該Touch Mesage ,需要自己實現(xiàn)以上幾個方法做派發(fā),例如

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    //do someThiing
  [self.nextResponser touchesBegan: touches withEvent:event];
}

事件轉(zhuǎn)發(fā)可以做很多事情。大家可以盡可能的想象

手勢處理

以上看來所有的事情都很平穩(wěn),無非就是尋找響應(yīng)鏈,傳遞事件等等。但是接下來大家可能需要蒙圈。先來道題目

  • AView 有子view BView,AView上面有一個單擊手勢,這個時候點擊BView。默認情況下,Bview的四個Touch方法中,那些方法會被調(diào)用?

可能很多人會說沒有任何影響,基本都會調(diào)用,答案是整個過程會調(diào)用這兩個方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

touchEnd不會被調(diào)用。
為什么?因為有手勢的存在,我們先看一下手勢。

手勢

手勢是蘋果為處理常用的用戶交互所推出了一個優(yōu)先級更高的處理技術(shù)。為了讓用戶完成對多種控件的基本操作,蘋果實現(xiàn)了以下幾個手勢

UITapGestureRecognizer
UIPinchGestureRecognizer
UIRotationGestureRecognizer
UISwipeGestureRecognizer
UIPanGestureRecognizer
UIScreenEdgePanGestureRecognizer
UILongPressGestureRecognizer

上面包括點擊,長按,旋轉(zhuǎn),滑動等等手勢。這樣開發(fā)者就可以隨便將其關(guān)聯(lián)到某個控件上完成交互。
先拋開剛才的問題,先看單純的手勢如何識別用戶操作。

系統(tǒng)會將用戶觸摸屏幕的點事件 發(fā)送給手勢,手勢會根據(jù)具體的點擊位置和序列,判斷是否是某種特定行為。具體的判斷方法如下

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

和UIResponser一樣,手勢也有這幾個方法,點擊的每個階段手勢都會響應(yīng)不同的方法,手勢會在以上四個方法中去對手勢的State做更改,手勢的State表明當(dāng)前手勢是識別還是失敗等等。比如單擊手勢會在touchesBegan 時記錄點擊位置,然后在touchesEnded判斷點擊次數(shù)、時間、是否移動過,最后得出否識別該手勢。這幾個方法一般在自定義手勢里面使用。

手勢狀態(tài)
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    //未知狀態(tài)
    UIGestureRecognizerStatePossible,   // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
    //首次識別狀態(tài),對于連續(xù)手勢,例如長按,有這種狀態(tài)
    UIGestureRecognizerStateBegan,      // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
    //再次識別,當(dāng)手連續(xù)手勢識別之后,再次受到touch事件
    UIGestureRecognizerStateChanged,    // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
    //識別完成,受到touchend 消息之后
    UIGestureRecognizerStateEnded,      // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
    //取消識別
    UIGestureRecognizerStateCancelled,  // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
    //識別失敗
    UIGestureRecognizerStateFailed,     // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
    // Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
    //識別狀態(tài),與識別結(jié)束一個意思
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};

手勢的狀態(tài)有以上幾種,官方給手勢狀態(tài)圖如下。


gestureStateChange.jpg

結(jié)合圖我們來看手勢的整個遷移過程,先明確幾個信息

  1. 手勢的狀態(tài)遷移只有在它們收到Touch message的時候,才能做狀態(tài)變化處理代碼。
  2. 手勢分為連續(xù)狀態(tài)手勢和不連續(xù)狀態(tài)手勢。連續(xù)手勢有長按,慢滑等。不連續(xù)手勢有單擊,雙擊等等。
  3. 當(dāng)用戶沒有點擊屏幕,所有手勢都處于Possiable狀態(tài)。
    當(dāng)用戶點擊屏幕,手勢會收到Touch Began Message, 手勢的touchBegan方法會被調(diào)用。手勢開始記錄點擊位置和時間。仍處于Possiable狀態(tài)。如果用戶按住不放,間隔超過一定時間,單擊手勢會變化為失敗狀態(tài),并在下個一runloop變?yōu)閜ossiable。如果時間大于長按手勢設(shè)定時間,長按手勢就會變化為Began狀態(tài),當(dāng)用戶移動手指,長按手勢的touch move方法被調(diào)用,長按手勢將自己狀態(tài)設(shè)置為Change,并且也會回調(diào)處理方法。最后手指離開,系統(tǒng)調(diào)用長按手勢touchEnd方法,手勢狀態(tài)回歸為Recognized狀態(tài)。

手勢混合處理

如果一個View上既有單擊,又有雙擊,用戶點擊該view兩次, 默認情況下,單擊被處理,雙擊不管用。因為默認情況下,一旦事件被某個手勢處理,第二個手勢會識別失敗 幸運的是蘋果提供了方法讓我們修改這種默認行為,具體的方法如下

@protocol UIGestureRecognizerDelegate <NSObject>
@optional
// called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

// called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
// return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
//
// note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

// called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

@end

上面是手勢的代理方法,你可以實現(xiàn)手勢的這幾個代理方法,更改默認行為。

  • (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
    手勢已經(jīng)應(yīng)分析出該事件可以響應(yīng),再對自己的狀態(tài)進行更改之前,會詢問代理的這個方法是否允許更改。默認為YES,如果你實現(xiàn)并設(shè)置為NO,那么手勢會變?yōu)槭顟B(tài),這個可以用在手勢只是別View的某幾個區(qū)域的相應(yīng)。
  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
    當(dāng)兩個手勢都對該事件進行識別,但只有一個能響應(yīng),另外一個會失敗。比如一個View上綁定兩個單擊事件。為了讓兩個手勢都響應(yīng),我們可以實現(xiàn)此方法,讓兩個手勢都響應(yīng)。
  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
    這兩個方法是iOS 7引入的,目的是讓兩個手勢之間增加依賴,比如單擊和雙擊,如果需要單擊在雙擊失敗的情況下識別,那么可以實現(xiàn)這兩個方法。
  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
    這兩個方法是判斷手勢在新的Touch和Press Began階段是否關(guān)注該UITouch和UIPress對象,默認為YES,如果設(shè)置為NO,手勢不會關(guān)注該Touch的任何狀態(tài)變化。
    這些函數(shù)的調(diào)用流程圖如下


    geture handle conflict.png

上圖只表明執(zhí)行順序,但是不一定每次每一個方法都被調(diào)用,實際要根據(jù)每個方法的實現(xiàn),來判斷是否要執(zhí)行其他方法

到此手勢和view單獨接受事件的情況完全介紹完。但是實際過程中二者是混合的,那么和這個時候會有什么情況發(fā)生呢?

手勢與事件相應(yīng)

回到我們上面問過的問題,BView只有touchBegan, touchesCancelle 的原因是什么?答案在于整個觸摸事件全過程
1.系統(tǒng)會通過hitTest的方法尋找響應(yīng)鏈,完成之后會形成下圖模型。


event handing squence.png

圖中最右邊是響應(yīng)鏈,中間是關(guān)聯(lián)在相應(yīng)鏈在視圖上的手勢

2.有了模型之后就會發(fā)生圖上的三個步驟
第一步:系統(tǒng)會將所有的 Touch message 優(yōu)先發(fā)送給 關(guān)聯(lián)在響應(yīng)鏈上的全部手勢。手勢根據(jù)Touch序列消息和手勢基本規(guī)則更改自己的狀態(tài)(有的可能失敗,有的可能識別等等)。如果沒有一個手勢對Touch message 進行攔截(攔截:系統(tǒng)不會將Touch message 發(fā)送給響應(yīng)鏈頂部響應(yīng)者),系統(tǒng)會進入第二步

第二步:系統(tǒng)將Touch message 發(fā)送給響應(yīng)鏈 頂部的 視圖控件,頂部視圖控件這個時候就會調(diào)用Touch相關(guān)的四個方法中的某一個。之后進入自定義Touch message轉(zhuǎn)發(fā)

第三步:自定義Touch message轉(zhuǎn)發(fā)可以繼承UIResponser的四個Touch函數(shù)做轉(zhuǎn)發(fā)。

解釋一下第一步中說的攔截,手勢會表明是否攔截該Touch Message,主要由下面三個屬性控制。

再回到那道題目,如果我們想hitTestView的toucheEnd函數(shù)依然能得到調(diào)用,怎么辦?其實UIGestureRecognizer有三個屬性

@property(nonatomic) BOOL cancelsTouchesInView;       // default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL delaysTouchesBegan;         // default is NO.  causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesEnded;         // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
  • cancelsTouchesInView
    默認為YES,表明當(dāng)手勢識別了該事件,系統(tǒng)會將Touch cancel消息發(fā)送給hitTestView ,并調(diào)用hitTestView的TouchCancel。設(shè)置為NO,不會再收到TouchCancel
  • delaysTouchesBegan
    默認為YES, 表明無論什么情況下,不會攔截Touch began消息。如果設(shè)置為NO,只要有一個手勢不識別失敗,都不會發(fā)送Touch began到響應(yīng)鏈的第一響應(yīng)者。
  • delaysTouchesEnded
    默認為NO, 和delaysTouchesBegan類似,不過它是用來控制TouchEnd message的攔截

總結(jié)

iOS整個事件處理的過程就是這樣,系統(tǒng)為完成整個交互做了很多東西,核心點如下:

  • 事件分發(fā)過程分為:1.尋找響應(yīng)鏈;2.事件消息分發(fā)
  • 響應(yīng)網(wǎng)是事件響應(yīng)的基礎(chǔ),響應(yīng)鏈?zhǔn)鞘录憫?yīng)的具體路徑。
  • 事件消息分發(fā)優(yōu)先發(fā)送給手勢集合,手勢內(nèi)部會做沖突處理,過濾消息。不被過濾的消息會傳遞給響應(yīng)鏈對象。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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