簡(jiǎn)介
iOS 事件分為三大類
- 觸摸事件
- 加速器事件
- 遠(yuǎn)程控制事件
以下我們講解觸摸事件
觸摸事件是我們平時(shí)遇到最多的事件,例如單擊、長(zhǎng)按、滑動(dòng)等等。當(dāng)用戶點(diǎn)擊按鈕,到按鈕處理回調(diào)。整個(gè)過程是如何發(fā)生,需要什么樣的原則,這些都是問題。為了使系統(tǒng)能更加鮮明符合用戶的操作邏輯,iOS系統(tǒng)將事件相應(yīng)過程拆分成兩部分:1.尋找響應(yīng)鏈;2.事件響應(yīng)。先將事件通過某種規(guī)則來(lái)分發(fā),找到處理事件的控件。其次是將事件傳遞分發(fā),響應(yīng)。
觸摸事件
UIEvent
iOS將觸摸事件定義為第一個(gè)手指開始觸摸屏幕到最后一個(gè)手指離開屏幕定義為一個(gè)觸摸事件。用類UIEvent表示。
UITouch
一個(gè)手指第一次點(diǎn)擊屏,會(huì)形成一個(gè)UITouch對(duì)象,直到離開銷毀。表示觸碰。UITouch對(duì)象能表明了當(dāng)前手指觸碰的屏幕位置,狀態(tài)。狀態(tài)分為開始觸碰、移動(dòng)、離開。
根據(jù)定義,UIEvent實(shí)際包括了多個(gè)UITouch對(duì)象。有幾個(gè)手指觸碰,就會(huì)有幾個(gè)UITouch對(duì)象。
代碼定義如下
@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對(duì)象的集合。
//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表明了手指移動(dòng)的狀態(tài),包括1.開始點(diǎn)擊;2.移動(dòng);3.保持; 4.離開;5.被取消(手指沒有離開屏幕,但是系統(tǒng)不再跟蹤它了)
綜上,UIEvent就是一組UITouch。每當(dāng)該組中任何一個(gè)UITouch對(duì)象的phase發(fā)生變化,系統(tǒng)都會(huì)產(chǎn)生一條TouchMessage。也就是說每次用戶手指的移動(dòng)和變化,UITouch都會(huì)形成狀態(tài)改變,系統(tǒng)變回會(huì)形成Touch message進(jìn)行傳遞和派發(fā)。那么 一次觸摸事件是由一組UITouch對(duì)象狀態(tài)變化引起的一組Touch message的轉(zhuǎn)發(fā)和派送。那么事件派發(fā)的原則是什么?
響應(yīng)鏈
響應(yīng)鏈?zhǔn)恰笆录砂l(fā)”的原則和規(guī)定,那么響應(yīng)鏈?zhǔn)鞘裁矗款櫭剂x事件鏈?zhǔn)且粋€(gè)鏈條,詳細(xì)的定義如下:
- 每條鏈?zhǔn)且粋€(gè) 鏈表狀結(jié)構(gòu),整個(gè)是一棵樹
- 鏈表的每一個(gè)node是一個(gè) UIResponser對(duì)象
先看下UIResponser,UIResponser是用來(lái)做什么的?
UIResponser就是用來(lái)接收和處理事件的類,先拋開iOS中的具體傳遞細(xì)節(jié),系統(tǒng)發(fā)送UIEvent的Touch message給UIResponser類。UIResponser提供了一下幾個(gè)函數(shù)來(lái)做事件處理
//觸摸事件
- (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 的處理,比如開始,移動(dòng),停止等等。常見的UIResponser有 UIView及子類,UIViController,APPDelegate,UIApplication等等。
回到響應(yīng)鏈,響應(yīng)鏈?zhǔn)怯蒛IResponser組成的,那么是按照哪種規(guī)則形成的。
- 程序啟動(dòng)
- UIApplication會(huì)生成一個(gè)單例,并會(huì)關(guān)聯(lián)一個(gè)APPDelegate。APPDelegate作為整個(gè)響應(yīng)鏈的根建立起來(lái),而UIApplication會(huì)將自己與這個(gè)單例鏈接,即UIApplication的nextResponser(下一個(gè)事件處理者)為APPDelegate。
- 創(chuàng)建UIWindow
- 程序啟動(dòng)后,任何的UIWindow被創(chuàng)建時(shí),UIWindow內(nèi)部都會(huì)把nextResponser設(shè)置為UIApplication單例。
- UIWindow初始化rootViewController, rootViewController的nextResponser會(huì)設(shè)置為UIWindow
- UIViewController初始化
- loadView, VC的view的nextResponser會(huì)被設(shè)置為VC.
- addSubView
- addSubView操作過程中,如果子subView不是VC的View,那么subView的nextResponser會(huì)被設(shè)置為superView。如果是VC的View,那就是 subView -> subView.VC ->superView
如果在中途,subView.VC被釋放,就會(huì)變成subView.nextResponser = superView
最終形成類似這樣一張圖
其中應(yīng)該是由箭頭的,箭頭的方向是超上,也就是subView指向superView.
事件傳遞
有了響應(yīng)網(wǎng)為基礎(chǔ),事件的傳遞就比較簡(jiǎn)單,只需要選擇其中一條響應(yīng)鏈,但是選擇那一條響應(yīng)鏈來(lái)傳遞呢?為了弄清真?zhèn)€過程,我們先來(lái)查看一下從觸摸硬件事件轉(zhuǎn)化為UIEvent消息。
- 首先用戶觸摸屏幕,系統(tǒng)的硬件進(jìn)程會(huì)獲取到這個(gè)點(diǎn)擊事件,將事件簡(jiǎn)單處理封裝后存到系統(tǒng)中,由于硬件檢測(cè)進(jìn)程和當(dāng)前運(yùn)行的APP是兩個(gè)進(jìn)程,所以進(jìn)程兩者之間傳遞事件用的是端口通信。硬件檢測(cè)進(jìn)程會(huì)將事件放入到APP檢測(cè)的那個(gè)端口。
- 其次,APP啟動(dòng)主線程RunLoop會(huì)注冊(cè)一個(gè)端口事件,來(lái)檢測(cè)觸摸事件的發(fā)生。當(dāng)時(shí)事件到達(dá),系統(tǒng)會(huì)喚起當(dāng)前APP主線程的Runloop。喚起原因就是端口觸摸事件,主線程會(huì)分析這個(gè)事件。
- 最后,系統(tǒng)判斷該次觸摸是否導(dǎo)致了一個(gè)新的事件, 也就是說是否是第一個(gè)手指開始觸碰,如果是,系統(tǒng)會(huì)先從響應(yīng)網(wǎng)中 尋找響應(yīng)鏈。如果不是,說明該事件是當(dāng)前正在進(jìn)行中的事件產(chǎn)生的一個(gè)Touch message, 也就是說已經(jīng)有保存好的響應(yīng)鏈。
如果是新事件,系統(tǒng)會(huì)尋找響應(yīng)鏈,為了符合用戶的操作習(xí)慣,系統(tǒng)會(huì)根據(jù)用戶的點(diǎn)擊位置,在當(dāng)前的整個(gè)APP的顯示層級(jí)中尋找。過程如下:
- 將所有的顯示在屏幕上的 "合格的"UIWindow對(duì)象 按照層級(jí)結(jié)構(gòu)從上到下排列成一個(gè)數(shù)組。
- 從第一個(gè)UIWindow對(duì)象開始,先判斷UIWindow是否合格,其次判斷 點(diǎn)擊位置在不在這個(gè)Window內(nèi),如果不在 ,返回nil, 就換下一個(gè)UIWindow;如果在的話,并且UIWindow沒有subView就返回自己,整個(gè)過程結(jié)束。如果UIWindow有subViews,就從后往前遍歷整個(gè)subViews,做和UIWindow類似的事情,直到找到一個(gè)View。如果沒有找到到就不做傳遞。
合格的UIWindow,UIView。意思是控件被允許接受事件。符合三個(gè)條件:1.不能被隱藏;2.alpha值大于0.01(不是backgroundColor為clearColor);3.isUserInteractionEnabled為YES,打開狀態(tài)。一般UILabel,UIImageView純顯示的控件默認(rèn)是關(guān)閉狀態(tài),也就是不處理事件。
顯示控件有了兩個(gè)方法來(lái)做上面這件事,就是常說的hitTest
// 先判斷點(diǎn)是否在View內(nèi)部,然后遍歷subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
//判斷點(diǎn)是否在這個(gè)View內(nèi)部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
整個(gè)過程的系統(tǒng)實(shí)現(xiàn)大致如下
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
//判斷是否合格
if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
//判斷點(diǎn)擊位置是否在自己區(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];
//對(duì)子view進(jìn)行hitTest
attachedView = [view hitTest:point withEvent:event];
if (attachedView)
break;
}
if (attachedView) {
return attachedView;
} else {
return self;
}
}
}
return nil;
}
技巧
以上可知默認(rèn)情況下,用戶點(diǎn)擊哪個(gè)View,系統(tǒng)就會(huì)在尋找過程中返回哪個(gè)view,但是我們可以重載上面兩個(gè)方法做如下事情:
- 將控件外部點(diǎn)規(guī)整到控件內(nèi)部。 例如控件較小,點(diǎn)擊位置在控件邊緣外部,可以重載- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; 將外部的點(diǎn)也判斷為內(nèi)部點(diǎn),這樣hitTest就會(huì)遍歷自己。
- 重載HitTest更改默認(rèn)行為。 有時(shí)候點(diǎn)擊subView的某些特殊位置需要superView處理,我們可以在superView的hitTest,返回superView。這樣superView變成首部響應(yīng)者
hitTest的邏輯代碼中會(huì)把隱藏,透明(alpha<0.01,不是backgroundColor為clearColor),不交互的view濾過,但不代表hitTest不會(huì)被調(diào)用,我們可以重載hitTest去讓 已經(jīng)隱藏、透明、不交互的view響應(yīng)事件。不過最正規(guī)的方法是打開控件交互屬性。
** 以上過程返回的View被稱作hitTestView,順著hitTestView的nextResponser,可以形成一個(gè)鏈,即響應(yīng)鏈。** 最后指向appDelegate. 并且返回hitTestView之后,系統(tǒng)會(huì)持有hitTestView。事件不結(jié)束,這個(gè)hitTestView不會(huì)發(fā)生變化,即使用戶點(diǎn)擊之后將手指移動(dòng)到其他控件上面,該點(diǎn)擊都會(huì)綁定開始的hitTestView。當(dāng)所有手指離開屏幕,事件結(jié)束。再次點(diǎn)擊,事件重新開始。以上過程再來(lái)一次。
事件響應(yīng)
形成響應(yīng)鏈之后,UIWindow會(huì)把事件目標(biāo)鎖定為hitTestView(響應(yīng)鏈頭的控件),當(dāng)手指狀態(tài)發(fā)生變化, 會(huì)不停的發(fā)送UITouch Message 給這個(gè)hitTestView。 下面這幾個(gè)方法會(huì)被調(diào)用。
然后控件的以下方法會(huì)陸續(xù)被調(diào)用
//點(diǎn)擊剛開始,回調(diào)這個(gè)方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//點(diǎn)擊之后移動(dòng),回調(diào)這個(gè)方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指移開,點(diǎn)擊結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//點(diǎn)擊過程中,事件被手勢(shì)識(shí)別,會(huì)回調(diào)這個(gè)方法,關(guān)于手勢(shì)后面會(huì)講解
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
技巧
由于系統(tǒng)只會(huì)把事件發(fā)送給 hitTestView,如果你想讓hitTestView之后的其他響應(yīng)者處理該Touch Mesage ,需要自己實(shí)現(xiàn)以上幾個(gè)方法做派發(fā),例如
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
//do someThiing
[self.nextResponser touchesBegan: touches withEvent:event];
}
事件轉(zhuǎn)發(fā)可以做很多事情。大家可以盡可能的想象
手勢(shì)處理
以上看來(lái)所有的事情都很平穩(wěn),無(wú)非就是尋找響應(yīng)鏈,傳遞事件等等。但是接下來(lái)大家可能需要蒙圈。先來(lái)道題目
- AView 有子view BView,AView上面有一個(gè)單擊手勢(shì),這個(gè)時(shí)候點(diǎn)擊BView。默認(rèn)情況下,Bview的四個(gè)Touch方法中,那些方法會(huì)被調(diào)用?
可能很多人會(huì)說沒有任何影響,基本都會(huì)調(diào)用,答案是整個(gè)過程會(huì)調(diào)用這兩個(gè)方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
touchEnd不會(huì)被調(diào)用。
為什么?因?yàn)橛惺謩?shì)的存在,我們先看一下手勢(shì)。
手勢(shì)
手勢(shì)是蘋果為處理常用的用戶交互所推出了一個(gè)優(yōu)先級(jí)更高的處理技術(shù)。為了讓用戶完成對(duì)多種控件的基本操作,蘋果實(shí)現(xiàn)了以下幾個(gè)手勢(shì)
UITapGestureRecognizer
UIPinchGestureRecognizer
UIRotationGestureRecognizer
UISwipeGestureRecognizer
UIPanGestureRecognizer
UIScreenEdgePanGestureRecognizer
UILongPressGestureRecognizer
上面包括點(diǎn)擊,長(zhǎng)按,旋轉(zhuǎn),滑動(dòng)等等手勢(shì)。這樣開發(fā)者就可以隨便將其關(guān)聯(lián)到某個(gè)控件上完成交互。
先拋開剛才的問題,先看單純的手勢(shì)如何識(shí)別用戶操作。
系統(tǒng)會(huì)將用戶觸摸屏幕的點(diǎn)事件 發(fā)送給手勢(shì),手勢(shì)會(huì)根據(jù)具體的點(diǎn)擊位置和序列,判斷是否是某種特定行為。具體的判斷方法如下
- (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一樣,手勢(shì)也有這幾個(gè)方法,點(diǎn)擊的每個(gè)階段手勢(shì)都會(huì)響應(yīng)不同的方法,手勢(shì)會(huì)在以上四個(gè)方法中去對(duì)手勢(shì)的State做更改,手勢(shì)的State表明當(dāng)前手勢(shì)是識(shí)別還是失敗等等。比如單擊手勢(shì)會(huì)在touchesBegan 時(shí)記錄點(diǎn)擊位置,然后在touchesEnded判斷點(diǎn)擊次數(shù)、時(shí)間、是否移動(dòng)過,最后得出否識(shí)別該手勢(shì)。這幾個(gè)方法一般在自定義手勢(shì)里面使用。
手勢(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
//首次識(shí)別狀態(tài),對(duì)于連續(xù)手勢(shì),例如長(zhǎng)按,有這種狀態(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
//再次識(shí)別,當(dāng)手連續(xù)手勢(shì)識(shí)別之后,再次受到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
//識(shí)別完成,受到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
//取消識(shí)別
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
//識(shí)別失敗
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
//識(shí)別狀態(tài),與識(shí)別結(jié)束一個(gè)意思
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
};
手勢(shì)的狀態(tài)有以上幾種,官方給手勢(shì)狀態(tài)圖如下。
結(jié)合圖我們來(lái)看手勢(shì)的整個(gè)遷移過程,先明確幾個(gè)信息
- 手勢(shì)的狀態(tài)遷移只有在它們收到Touch message的時(shí)候,才能做狀態(tài)變化處理代碼。
- 手勢(shì)分為連續(xù)狀態(tài)手勢(shì)和不連續(xù)狀態(tài)手勢(shì)。連續(xù)手勢(shì)有長(zhǎng)按,慢滑等。不連續(xù)手勢(shì)有單擊,雙擊等等。
- 當(dāng)用戶沒有點(diǎn)擊屏幕,所有手勢(shì)都處于Possiable狀態(tài)。
當(dāng)用戶點(diǎn)擊屏幕,手勢(shì)會(huì)收到Touch Began Message, 手勢(shì)的touchBegan方法會(huì)被調(diào)用。手勢(shì)開始記錄點(diǎn)擊位置和時(shí)間。仍處于Possiable狀態(tài)。如果用戶按住不放,間隔超過一定時(shí)間,單擊手勢(shì)會(huì)變化為失敗狀態(tài),并在下個(gè)一runloop變?yōu)閜ossiable。如果時(shí)間大于長(zhǎng)按手勢(shì)設(shè)定時(shí)間,長(zhǎng)按手勢(shì)就會(huì)變化為Began狀態(tài),當(dāng)用戶移動(dòng)手指,長(zhǎng)按手勢(shì)的touch move方法被調(diào)用,長(zhǎng)按手勢(shì)將自己狀態(tài)設(shè)置為Change,并且也會(huì)回調(diào)處理方法。最后手指離開,系統(tǒng)調(diào)用長(zhǎng)按手勢(shì)touchEnd方法,手勢(shì)狀態(tài)回歸為Recognized狀態(tài)。
手勢(shì)混合處理
如果一個(gè)View上既有單擊,又有雙擊,用戶點(diǎn)擊該view兩次, 默認(rèn)情況下,單擊被處理,雙擊不管用。因?yàn)?strong>默認(rèn)情況下,一旦事件被某個(gè)手勢(shì)處理,第二個(gè)手勢(shì)會(huì)識(shí)別失敗 幸運(yùn)的是蘋果提供了方法讓我們修改這種默認(rèn)行為,具體的方法如下
@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
上面是手勢(shì)的代理方法,你可以實(shí)現(xiàn)手勢(shì)的這幾個(gè)代理方法,更改默認(rèn)行為。
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
手勢(shì)已經(jīng)應(yīng)分析出該事件可以響應(yīng),再對(duì)自己的狀態(tài)進(jìn)行更改之前,會(huì)詢問代理的這個(gè)方法是否允許更改。默認(rèn)為YES,如果你實(shí)現(xiàn)并設(shè)置為NO,那么手勢(shì)會(huì)變?yōu)槭顟B(tài),這個(gè)可以用在手勢(shì)只是別View的某幾個(gè)區(qū)域的相應(yīng)。 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
當(dāng)兩個(gè)手勢(shì)都對(duì)該事件進(jìn)行識(shí)別,但只有一個(gè)能響應(yīng),另外一個(gè)會(huì)失敗。比如一個(gè)View上綁定兩個(gè)單擊事件。為了讓兩個(gè)手勢(shì)都響應(yīng),我們可以實(shí)現(xiàn)此方法,讓兩個(gè)手勢(shì)都響應(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);
這兩個(gè)方法是iOS 7引入的,目的是讓兩個(gè)手勢(shì)之間增加依賴,比如單擊和雙擊,如果需要單擊在雙擊失敗的情況下識(shí)別,那么可以實(shí)現(xiàn)這兩個(gè)方法。 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
這兩個(gè)方法是判斷手勢(shì)在新的Touch和Press Began階段是否關(guān)注該UITouch和UIPress對(duì)象,默認(rèn)為YES,如果設(shè)置為NO,手勢(shì)不會(huì)關(guān)注該Touch的任何狀態(tài)變化。
這些函數(shù)的調(diào)用流程圖如下
geture handle conflict.png
上圖只表明執(zhí)行順序,但是不一定每次每一個(gè)方法都被調(diào)用,實(shí)際要根據(jù)每個(gè)方法的實(shí)現(xiàn),來(lái)判斷是否要執(zhí)行其他方法
到此手勢(shì)和view單獨(dú)接受事件的情況完全介紹完。但是實(shí)際過程中二者是混合的,那么和這個(gè)時(shí)候會(huì)有什么情況發(fā)生呢?
手勢(shì)與事件相應(yīng)
回到我們上面問過的問題,BView只有touchBegan, touchesCancelle 的原因是什么?答案在于整個(gè)觸摸事件全過程
1.系統(tǒng)會(huì)通過hitTest的方法尋找響應(yīng)鏈,完成之后會(huì)形成下圖模型。
圖中最右邊是響應(yīng)鏈,中間是關(guān)聯(lián)在相應(yīng)鏈在視圖上的手勢(shì)
2.有了模型之后就會(huì)發(fā)生圖上的三個(gè)步驟
第一步:系統(tǒng)會(huì)將所有的 Touch message 優(yōu)先發(fā)送給 關(guān)聯(lián)在響應(yīng)鏈上的全部手勢(shì)。手勢(shì)根據(jù)Touch序列消息和手勢(shì)基本規(guī)則更改自己的狀態(tài)(有的可能失敗,有的可能識(shí)別等等)。如果沒有一個(gè)手勢(shì)對(duì)Touch message 進(jìn)行攔截(攔截:系統(tǒng)不會(huì)將Touch message 發(fā)送給響應(yīng)鏈頂部響應(yīng)者),系統(tǒng)會(huì)進(jìn)入第二步
第二步:系統(tǒng)將Touch message 發(fā)送給響應(yīng)鏈 頂部的 視圖控件,頂部視圖控件這個(gè)時(shí)候就會(huì)調(diào)用Touch相關(guān)的四個(gè)方法中的某一個(gè)。之后進(jìn)入自定義Touch message轉(zhuǎn)發(fā)
第三步:自定義Touch message轉(zhuǎn)發(fā)可以繼承UIResponser的四個(gè)Touch函數(shù)做轉(zhuǎn)發(fā)。
解釋一下第一步中說的攔截,手勢(shì)會(huì)表明是否攔截該Touch Message,主要由下面三個(gè)屬性控制。
再回到那道題目,如果我們想hitTestView的toucheEnd函數(shù)依然能得到調(diào)用,怎么辦?其實(shí)UIGestureRecognizer有三個(gè)屬性
@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
默認(rèn)為YES,表明當(dāng)手勢(shì)識(shí)別了該事件,系統(tǒng)會(huì)將Touch cancel消息發(fā)送給hitTestView ,并調(diào)用hitTestView的TouchCancel。設(shè)置為NO,不會(huì)再收到TouchCancel - delaysTouchesBegan
默認(rèn)為YES, 表明無(wú)論什么情況下,不會(huì)攔截Touch began消息。如果設(shè)置為NO,只要有一個(gè)手勢(shì)不識(shí)別失敗,都不會(huì)發(fā)送Touch began到響應(yīng)鏈的第一響應(yīng)者。 - delaysTouchesEnded
默認(rèn)為NO, 和delaysTouchesBegan類似,不過它是用來(lái)控制TouchEnd message的攔截
總結(jié)
iOS整個(gè)事件處理的過程就是這樣,系統(tǒng)為完成整個(gè)交互做了很多東西,核心點(diǎn)如下:
- 事件分發(fā)過程分為:1.尋找響應(yīng)鏈;2.事件消息分發(fā)
- 響應(yīng)網(wǎng)是事件響應(yīng)的基礎(chǔ),響應(yīng)鏈?zhǔn)鞘录憫?yīng)的具體路徑。
- 事件消息分發(fā)優(yōu)先發(fā)送給手勢(shì)集合,手勢(shì)內(nèi)部會(huì)做沖突處理,過濾消息。不被過濾的消息會(huì)傳遞給響應(yīng)鏈對(duì)象。