iOS觸摸事件處理

在開發(fā)過程中,大家或多或少的都會(huì)碰到令人頭疼的手勢(shì)沖突問題,正好前兩天碰到一個(gè)類似的bug,于是借著這個(gè)機(jī)會(huì)了解了iOS中的事件傳遞與處理的相關(guān)內(nèi)容,整理出來方便以后查閱。

iPhone的成功,很大的一部分在于用戶可以以多種方式操縱他們的設(shè)備。大體上iOS的事件分為三類:觸摸事件(手勢(shì)操作),運(yùn)動(dòng)事件(搖一搖),遠(yuǎn)程控制事件(耳機(jī)線控),本文主要整理的是觸摸事件,對(duì)其它兩種就不多做介紹了,感興趣的同學(xué)可以自己查閱資料。

事件的生命周期

從手指觸摸屏幕,觸摸事件的傳遞大概經(jīng)歷了3個(gè)階段,系統(tǒng)響應(yīng)階段-->SpringBoard.app處理階段-->前臺(tái)App處理階段,大致的流程如下圖:
uitouchflow.png
起始階段
  • cpu處于睡眠階段,等待事件發(fā)生
  • 手指觸摸屏幕
系統(tǒng)響應(yīng)階段
  • 屏幕感應(yīng)到觸摸事件,并將感應(yīng)到的事件傳遞給IOKit(用來操作硬件和驅(qū)動(dòng)的框架,這是一個(gè)私有API,知道這個(gè)是干嘛的就行了)
  • IOKit.framework封裝整個(gè)觸摸事件為IOHIDEvent對(duì)象,直接通過mach port(Mach屬于硬件層,僅提供了諸如處理器調(diào)度、IPC進(jìn)程通信等非常少量的基礎(chǔ)服務(wù)。在Mach中,所有的東西都是通過自己的對(duì)象實(shí)現(xiàn)的,進(jìn)程、線程和虛擬內(nèi)存都被稱為“對(duì)象”,Mach的對(duì)象間不能直接調(diào)用,只能通過消息傳遞的方式實(shí)現(xiàn)對(duì)象間的通信。消息是Mach中最基礎(chǔ)的概念,消息在兩個(gè)端口(port)之間傳遞。mach port就是IPC進(jìn)程間通信的核心,更多內(nèi)容請(qǐng)查看這篇文章)轉(zhuǎn)發(fā)給SpringBoard.app。
SpringBoard.app處理階段
  • SpringBoard.app的主線程Runloop收到IOKit.framework轉(zhuǎn)發(fā)來的消息蘇醒,并觸發(fā)對(duì)應(yīng)mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()。
  • 如果SpringBoard.app監(jiān)測(cè)到有App在前臺(tái)(記為xxx.app),SpringBoard.app再通過mach port轉(zhuǎn)發(fā)給xxx.app,如果SpringBoard.app監(jiān)測(cè)到前臺(tái)沒有App運(yùn)行,則SpringBoard.app進(jìn)入App內(nèi)部響應(yīng)階段,觸發(fā)自身主線程runloop的Source0時(shí)間源的回調(diào)。

SpringBoard.app是一個(gè)系統(tǒng)進(jìn)程,可以理解為桌面系統(tǒng),可以統(tǒng)一管理和分發(fā)系統(tǒng)接收到的觸摸事件。

App內(nèi)部響應(yīng)階段
  • 前臺(tái)App主線程Runloop收到SpringBoard.app轉(zhuǎn)發(fā)來的消息而蘇醒,并觸發(fā)對(duì)應(yīng)mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()。
  • Source1回調(diào)內(nèi)部,觸發(fā)Source0回調(diào)__UIApplicationHandleEventQueue()
  • Source0回調(diào)內(nèi)部,封裝IOHIDEvent為UIEvent。
  • Source0回調(diào)內(nèi)部,調(diào)用UIApplication的sendEvent:方法,將UIEvent傳給UIWindow,接下來就是尋找最佳響應(yīng)者的過程,也就是命中測(cè)試hit-testing。
  • 尋找到最佳響應(yīng)者后,接下來就是事件在響應(yīng)鏈中的傳遞和響應(yīng)了。需要注意的是,事件除了可以被響應(yīng)者處理之外,還有可能被手勢(shì)識(shí)別器或者target-action捕捉并處理,這涉及到一個(gè)優(yōu)先級(jí)的問題。如果觸摸事件在響應(yīng)鏈中沒有找到能夠響應(yīng)該事件的對(duì)象,最終將被釋放。
  • 事件被處理或者釋放之后,runloop如果沒有其他事件進(jìn)行處理,將會(huì)再次進(jìn)入休眠狀態(tài)。

Source0和Source1都可用于線程(或進(jìn)程)交互,但交互的形式有所不同,Source1監(jiān)聽端口,當(dāng)端口有消息到達(dá)時(shí),響應(yīng)的Source1就會(huì)被觸發(fā)回調(diào),完成響應(yīng)的操作;而Source0并不監(jiān)聽端口,讓Source0執(zhí)行回調(diào)需要手動(dòng)標(biāo)記Source0為待處理狀態(tài),還需要呼醒Source0所在的Runloop。從Source1和Source0的交互方式了解到,Source1的交互會(huì)主動(dòng)呼醒所在的Runloop,而Source0的交互則需要依賴其他線程來呼醒Source0所在的Runloop。一次Runloop只能執(zhí)行一個(gè)Source1的回調(diào),但可以執(zhí)行多個(gè)待處理的Source0的回調(diào)。

尋找事件的最佳響應(yīng)者(Hit-Testing)

能夠響應(yīng)觸摸事件的例如UIView,UIButton,UIViewController,UIApplication,Appdelegate等都繼承自UIResponder類,一個(gè)頁(yè)面上通常會(huì)有許許多多個(gè)這種類型的對(duì)象,都可以對(duì)點(diǎn)擊事件作出響應(yīng)。為了避免沖突,這就需要有一個(gè)先后順序,也就是響應(yīng)的優(yōu)先級(jí)。Hit-Testing的目的就是找到具有最高優(yōu)先級(jí)的響應(yīng)對(duì)象。
尋找的具體流程如下:

  1. UIApplication首先將事件隊(duì)列中的事件取出,傳遞給窗口對(duì)象。如果有多個(gè)窗口,則優(yōu)先詢問windows數(shù)組的最后一個(gè)窗口。
  2. 如果窗口不能響應(yīng)事件,則將事件傳遞給倒數(shù)第二個(gè)窗口,以此類推。如果窗口能夠響應(yīng)事件,則再依次詢問該窗口的子視圖。
  3. 重復(fù)步驟2。
  4. 若視圖的所有子視圖均不是最佳響應(yīng)者,則自身就是最合適的響應(yīng)者。
    另外需要注意的是,一下幾種狀態(tài)的視圖無法響應(yīng)事件:
  • 不允許交互的視圖:userInteractionEnabled = NO
  • 隱藏的視圖:hidden = YES
  • 透明度alpha<0.01的視圖

怎么樣驗(yàn)證一下上面所說的Hit-Testing的順序呢,看一下UIView的API,里面會(huì)有一個(gè)hitTest:withEvent:方法,這個(gè)方法的主要作用就是查詢并返回事件在當(dāng)前視圖中的響應(yīng)者,每個(gè)被詢問到的視圖對(duì)象都會(huì)調(diào)用這個(gè)方法來返回當(dāng)前視圖層的響應(yīng)者。

  • 如果當(dāng)前視圖無法響應(yīng)事件,則返回nil。
  • 如果當(dāng)前視圖可以響應(yīng)事件,但子視圖不能響應(yīng)事件,則返回自身作為當(dāng)前視圖的響應(yīng)者。
  • 如果當(dāng)前視圖可以響應(yīng)事件,同時(shí)有子視圖可以響應(yīng)事件,則返回該子視圖作為當(dāng)前視圖的響應(yīng)者。

所以我們可以根據(jù)通過觀察該方法的調(diào)用順序,來確定Hit-Testing的順序。


屏幕快照 2017-11-27 下午2.32.23.png

如圖所示,A視圖上面添加了子視圖B和C,B上面添加了子視圖D,C上面添加了子視圖E和F。創(chuàng)建一個(gè)繼承自UIView的類HTView,重寫hitTest:withEvent:方法:

@interface HTView : UIView
@property (nonatomic, strong) NSString *name; //視圖的名字
@end

@implementation HTView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"進(jìn)入%@視圖-%s", self.name, __func__);
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"離開%@視圖-%s", self.name, __func__);
    [圖片上傳中...(屏幕快照 2017-11-27 下午3.58.14.png-334090-1511769555186-0)]

return view;
}
@end

在ViewController中添加如下代碼:

#import "ViewController.h"
#import "HTView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet HTView *aView;
@property (weak, nonatomic) IBOutlet HTView *bView;
@property (weak, nonatomic) IBOutlet HTView *cView;
@property (weak, nonatomic) IBOutlet HTView *dView;
@property (weak, nonatomic) IBOutlet HTView *eView;
@property (weak, nonatomic) IBOutlet HTView *fView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.aView.name = @"A";
    self.bView.name = @"B";
    self.cView.name = @"C";
    self.dView.name = @"D";
    self.eView.name = @"E";
    self.fView.name = @"F";
}
@end

點(diǎn)擊E視圖,打印的結(jié)果如下:


屏幕快照 2017-11-27 下午4.16.08.png

由打印的結(jié)果可知:

  1. 事件首先傳遞給視圖A。
  2. A判斷自身能響應(yīng)事件,繼續(xù)從后向前遍歷A的子視圖,因?yàn)镃比B后添加,因此首先傳遞給C。
  3. C判斷自身能響應(yīng)事件,繼續(xù)從后向前遍歷C的子視圖,因?yàn)镕比E后添加,因此首先傳遞給F。
  4. F判斷自身不能響應(yīng)事件,C又將事件傳遞給E。
  5. E判斷自身能響應(yīng)事件,同時(shí)E已經(jīng)沒有子視圖,因此最終E就是最佳響應(yīng)者。

(這里有一個(gè)問題,為什么遍歷視圖的時(shí)候需要從后往前遍歷呢?為什么B和C都是A的子視圖,判斷出了C視圖能響應(yīng)事件之后,B視圖沒有繼續(xù)調(diào)用hitTest:withEvent:方法呢?)

那么視圖又是怎么判斷自身是否可以響應(yīng)事件的呢?答案是通過poingInside:withEvent這個(gè)方法來判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)。那么結(jié)合上面的hitTest調(diào)用的相關(guān)知識(shí)來看,hitTest:withEvent方法的大概實(shí)現(xiàn)已經(jīng)呼之欲出了:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 先判斷視圖是否處于不能響應(yīng)事件的3種狀態(tài)
    if (self.userInteractionEnabled == NO || self.hidden || self.alpha < 0.01) {
        return nil;
    }
    // 判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)
    if ([self pointInside:point withEvent:event] == NO) {
        return nil;
    }
    // 從后向前遍歷視圖的子視圖
    for (int i = (int)self.subviews.count - 1; i >= 0; i--) {
        UIView *subView = self.subviews[i];
        // 坐標(biāo)轉(zhuǎn)換,把觸摸點(diǎn)的位置轉(zhuǎn)換為子視圖坐標(biāo)系下的坐標(biāo)
        CGPoint subPoint = [self convertPoint:point toView:subView];
        // 對(duì)子視圖進(jìn)行Hit-Testing
        UIView *subHTView = [subView hitTest:subPoint withEvent:event];
        // 如果子視圖有最佳響應(yīng)者,返回該最佳響應(yīng)者視圖,結(jié)束循環(huán)
        if (subHTView) {
            return subHTView;
        }
    }
    // 如果子視圖中沒有最佳響應(yīng)者,返回自己
    return self;
}

重新點(diǎn)擊,發(fā)現(xiàn)視圖仍然可以正常響應(yīng)點(diǎn)擊事件,證明我們所寫的實(shí)現(xiàn)與系統(tǒng)的方法基本相同。這里我們就可以回答上面括號(hào)里面的問題了,為什么要從后往前遍歷呢?因?yàn)閿?shù)組里面后面的視圖是后添加的,后添加的視圖一般都是在視圖的上層,會(huì)把先添加的視圖遮擋,我們自然不會(huì)想要去點(diǎn)擊被遮擋住的位置。為什么B視圖沒有調(diào)用hitTest:withEvent:方法呢?因?yàn)橐呀?jīng)確定觸摸點(diǎn)在C視圖上了。如果B和C沒有重疊部分,自然不用再判斷B視圖能否響應(yīng),如果有重疊部分,后添加的C自然是在上層,所以C優(yōu)先響應(yīng),也不會(huì)再對(duì)B視圖進(jìn)行判斷。
我們通過這段代碼還可以解釋另外一種現(xiàn)象,子視圖超出了父視圖的范圍,點(diǎn)擊子視圖在父視圖之外的部分沒有反應(yīng)。這是因?yàn)樵谶M(jìn)行Hit-Testing的時(shí)候,父視圖就已經(jīng)判斷自己不能響應(yīng)事件了,自然不會(huì)再去詢問子視圖是否能夠響應(yīng)事件。

如果碰到這種需求怎么辦?比如說tabBar中間的按鈕凸起


FD64ADCA-17B2-496B-AC09-5A7AEEDFF183.png

這時(shí)候就需要重寫父視圖的pointInside:withEvent:方法了,在tabBar中判斷當(dāng)前觸摸位置是否在中間凸起的按鈕的坐標(biāo)范圍內(nèi),如果在,就返回YES。這樣得以讓觸摸事件傳遞到中間的按鈕上,并確定按鈕為最佳響應(yīng)者。代碼如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //將觸摸點(diǎn)坐標(biāo)轉(zhuǎn)換到在circleButton上的坐標(biāo)
    CGPoint subPoint = [self convertPoint:point toView:self.circleButton];
    //若觸摸點(diǎn)circleButton上則返回YES
    if ([self.circleButton pointInside:subPoint withEvent:event])       {
        return YES;
    }
    //否則返回默認(rèn)的操作
    return [super pointInside:point withEvent:event];
}

這里還有另一個(gè)問題,為什么Hit-Testing過程進(jìn)行了兩次?
剛開始的猜想是兩次執(zhí)行時(shí)的參數(shù)event不一樣,點(diǎn)擊A視圖的父視圖,打印event對(duì)象:


1A71FACD-C456-4EA5-8290-FD9FA5805341.png
1A71FACD-C456-4EA5-8290-FD9FA5805341.png

從結(jié)果可以看到,兩次的event對(duì)象地址一樣,且allTouches集合里面沒有UITouch對(duì)象(看到有的簡(jiǎn)友提到兩次調(diào)用的原因是UITouch對(duì)象的狀態(tài)不同,一次是begin,一次是end,這一點(diǎn)我持懷疑態(tài)度,因?yàn)闆]法驗(yàn)證。看上面提到的App響應(yīng)階段的操作,source0把IOHIDEvent對(duì)象封裝成了UIEvent對(duì)象,再結(jié)合這里hitTest:withEvent:方法里面沒有UITouch對(duì)象,然后我們還可以看一下touchesBegan方法里面是有touches對(duì)象的,我猜測(cè)UITouch對(duì)象是依據(jù)UIEvent對(duì)象的某些屬性生成的,這個(gè)過程發(fā)生在Hit-Testing過程之后。既然UITouch對(duì)象都沒有生成,那就更談不上UITouch狀態(tài)的變化了,當(dāng)然這里是我的臆想,希望有大神看到之后進(jìn)行指正)。然后我在hitTest:withEvent:里面打了一個(gè)斷點(diǎn),在event里面發(fā)現(xiàn)了這么一個(gè)東西:


20248863-DD94-418F-889F-1C94BABCF5F9.png

類型是_IOHIDEvent。這不就是最開始封裝成UIEvent那個(gè)類嗎,觸摸事件的相關(guān)信息應(yīng)該就儲(chǔ)存在這個(gè)成員變量里面。這是一個(gè)私有成員變量,直接取是取不出來的,不過這難不倒我們,我們有runtime可以取。在hitTest:withEvent:里面添加如下代碼:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    Ivar ivar = class_getInstanceVariable([event class], "_hidEvent");
    NSLog(@"%@", object_getIvar(event, ivar));
    return [super hitTest:point withEvent:event];
}

點(diǎn)擊A視圖的父視圖(這里只會(huì)對(duì)A進(jìn)行l(wèi)兩次hitTest,方便我們查看),第一次打印:

0B0483B1-E4A8-4EBB-859F-2A34159A7E4D.png

第二次打印:

7646AC0D-018F-4E5A-9D87-2F45ACE6F813.png

然后滿懷希望的一項(xiàng)一項(xiàng)比對(duì)里面的信息,貌似除了Total Latency(總延時(shí))之外都是一樣的,而且這里面好多字段我也不知道什么意思,找了半天也沒有找到_IOHIDEvent的API(有這方面資料的同學(xué)請(qǐng)不吝賜教)。按照這種方式把event里面的成員變量一個(gè)個(gè)打印,大部分都是空數(shù)組或者空字典。看來這兩次hitTest:withEvent:的區(qū)別不在于參數(shù)上面。那么會(huì)不會(huì)在方法的調(diào)用順序上面有區(qū)別呢?依然是點(diǎn)擊A視圖的父視圖,看一下方法調(diào)用棧:

第一次調(diào)用棧
第二次調(diào)用棧

可以看到兩次調(diào)用的不同就在于紅框圈出來的部分。第一次有對(duì)UIWindow進(jìn)行Hit-Testing,第二次沒有,而是直接對(duì)UIView進(jìn)行了Hit-Testing。至于其中的原因,我也不知道。。。。。扯了那么多,也沒得出個(gè)結(jié)果,不要打我。

事件的響應(yīng)及在響應(yīng)鏈中的傳遞

經(jīng)歷Hit-Testing后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了,接下來要做的兩件事情就是:

  1. 將事件傳遞給最佳響應(yīng)者響應(yīng)。
  2. 事件沿著響應(yīng)鏈傳遞,直到有UIResponder對(duì)象對(duì)此事件負(fù)責(zé)。
事件響應(yīng)的前奏

因?yàn)樽罴秧憫?yīng)者具有最高的事件響應(yīng)優(yōu)先級(jí),因此UIApplication會(huì)先將事件傳遞給它供其響應(yīng)。首先,UIApplication將事件通過sendEvent:傳遞給事件所屬的window,window同樣通過sendEvent:再將事件傳遞給最佳響應(yīng)者。在自定義的View里面重寫touchesBegan:方法,打上斷點(diǎn),可以看到調(diào)用棧如下:

touchesBegan調(diào)用棧

那么UIApplication和UIWindow又是怎么知道應(yīng)該把事件發(fā)送給哪個(gè)視圖的呢?我們可以看一下touches里面的UITouch對(duì)象的屬性。里面有window和view兩個(gè)字段,分別代表事件分發(fā)的UIWindow和最佳響應(yīng)者的地址(需要注意的是這個(gè)地址并不代表最終響應(yīng)事件的UIResponder地址,只是會(huì)最先分發(fā)給它,它也可以不對(duì)事件作出響應(yīng))。還有一個(gè)字段gestureRecongnizers,這里面存儲(chǔ)了響應(yīng)鏈上的視圖上面添加的手勢(shì)(這里面的東西在后面講到的手勢(shì)識(shí)別器會(huì)有涉及)。


FEA6ED47-036A-4086-94D5-802D11DFF86D.png
事件的響應(yīng)

響應(yīng)者鏈上面的每個(gè)響應(yīng)者都是繼承于UIResponder的對(duì)象,每個(gè)UIResponder對(duì)象都默認(rèn)實(shí)現(xiàn)了4個(gè)響應(yīng)觸摸事件的方法:

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

系統(tǒng)默認(rèn)的方法不對(duì)事件做任何處理,只是將事件沿著響應(yīng)者鏈傳遞。如果想要截獲事件進(jìn)行自定義的響應(yīng)操作,就要重寫相關(guān)的方法。例如:重寫touchesMoved方法實(shí)現(xiàn)簡(jiǎn)單的視圖拖動(dòng)。

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    CGPoint prePoint = [touch previousLocationInView:self.view];
    self.aView.transform = CGAffineTransformTranslate(self.aView.transform, point.x - prePoint.x, point.y - prePoint.y);
}

該方法在父視圖和子視圖中重寫會(huì)有不同的效果,如果在父視圖中重寫,點(diǎn)擊位置在你要移動(dòng)的子視圖之外也可以移動(dòng)。如果在子視圖中重寫,則點(diǎn)擊位置需要在子視圖的范圍之內(nèi)才可以移動(dòng)。

事件在響應(yīng)者鏈上的傳遞

前面一直在提最佳響應(yīng)者,之所以稱為"最佳",是因?yàn)槠渚邆漤憫?yīng)事件的最高優(yōu)先權(quán)(響應(yīng)鏈頂端的男人)。最佳響應(yīng)者首先接收到事件,然后便擁有了對(duì)事件的絕對(duì)控制權(quán):它既可以選擇獨(dú)吞這個(gè)事件,也可以將這個(gè)事件往下傳遞給其它響應(yīng)者,這個(gè)由響應(yīng)者構(gòu)成的鏈就稱之為響應(yīng)者鏈。
需要注意的是,上面也說到了事件的傳遞,這與此處所說的事件的傳遞有本質(zhì)的區(qū)別。上面所說的事件的傳遞的目的是為了尋找事件的最佳響應(yīng)者,是自下而上的傳遞。而這里的事件傳遞的目的是響應(yīng)者對(duì)事件作出響應(yīng),這個(gè)過程是自上而下的,前者為"尋找",后者為"響應(yīng)"。
響應(yīng)者對(duì)于事件的操作方式:
響應(yīng)者對(duì)于事件的攔截以及傳遞都是通過touchesBegan:withEvent:方法控制的,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞。(如果你在不同的UIResponder對(duì)象上面都聲明了touchMoved方法,那么這些對(duì)象都可以執(zhí)行該方法,因?yàn)閠ouchesBegan方法默認(rèn)是把事件沿著響應(yīng)鏈傳遞的。如果只想讓一個(gè)對(duì)象響應(yīng)touchesMoved方法,需要重寫touchesBegan方法以攔截事件)
響應(yīng)者對(duì)于接收到的事件有3種操作:

  • 不攔截,默認(rèn)操作
    事件會(huì)自動(dòng)沿著默認(rèn)的響應(yīng)鏈向下傳遞
  • 攔截,不再往下分發(fā)事件
    重寫touchesBegan:withEvent:方法進(jìn)行事件處理,不調(diào)用父類的touchesBegan:withEvent:方法
  • 攔截,繼續(xù)往下分發(fā)事件
    重寫touchesBegan:withEvent:進(jìn)行事件處理,同時(shí)調(diào)用父類的touchesBegan:withEvent:將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則:

每一個(gè)響應(yīng)者對(duì)象(UIResponder對(duì)象)都有一個(gè)nextResponder方法,用于獲取響應(yīng)鏈中當(dāng)前對(duì)象的下一個(gè)響應(yīng)者。因此,一旦事件的最佳響應(yīng)者確定了,這個(gè)事件所處的響應(yīng)鏈就確定了。

對(duì)于響應(yīng)者對(duì)象,默認(rèn)的nextResponder實(shí)現(xiàn)如下:

  • UIView
    若視圖是控制器的根視圖,則其nextResponder為控制器對(duì)象;否則,其nextResponder為父視圖。
  • UIViewController
    若控制器的視圖是window的根視圖,則其nextResponder為窗口對(duì)象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。
  • UIWindow
    nextResponder為UIApplication對(duì)象
  • UIApplication
    若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對(duì)象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate
  • AppDelegate
    nextResponder為nil

792F789A-7322-4E73-A92D-513340F7AFB4.png

上圖是官網(wǎng)對(duì)于響應(yīng)鏈的示例展示,若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:
UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對(duì)象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對(duì)象。

可以通過重寫UIResponder對(duì)象的- (nullable UIResponder*)nextResponder;方法改變響應(yīng)者鏈,但是UIResponder對(duì)象的nextResponder屬性是只讀屬性,不能直接賦值。

UIGestureRecognizer

在iOS中有六種手勢(shì)操作:
UITapGestureRecognizer 點(diǎn)按手勢(shì)
UIPinchGestureRecognizer 捏合手勢(shì)
UIPanGestureRecognizer 拖動(dòng)手勢(shì)
UISwipeGestureRecognizer 輕掃手勢(shì),支持四個(gè)方向的輕掃,但是不同的方向要分別定義輕掃手勢(shì)
UIRotationGestureRecognizer 旋轉(zhuǎn)手勢(shì)
UILongPressGestureRecognizer 長(zhǎng)按手勢(shì)
所有的手勢(shì)操作都繼承于UIGestureRecognizer,這個(gè)類本身不能直接使用。這個(gè)類中定義了這幾種手勢(shì)公有的一些屬性和方法。


CFFB9CE8-59D3-4DDB-B9D7-70D4E77702A8.png
手勢(shì)狀態(tài)

這里著重解釋一下上表中手勢(shì)狀態(tài)這個(gè)對(duì)象。在六種手勢(shì)識(shí)別中,只有一種手勢(shì)是離散手勢(shì),它就是UITapGestureRecognizer。離散手勢(shì)的特點(diǎn)就是一旦識(shí)別就無法取消,而且只會(huì)調(diào)用一次手勢(shì)操作(初始化手勢(shì)時(shí)指定的觸發(fā)方法)。其它五種手勢(shì)是連續(xù)手勢(shì),連續(xù)手勢(shì)的特點(diǎn)就是會(huì)多次調(diào)用手勢(shì)操作事件,而且在連續(xù)手勢(shì)識(shí)別后可以取消手勢(shì)。從下面兩圖中可以看出兩者調(diào)用操作事件的次數(shù)是不同的:


471B0C89-9B09-4A78-8607-2916464989B6.png

在iOS中將手勢(shì)狀態(tài)分為如下幾種:


2D616318-E45F-43B0-B651-CB70C5037F76.png
  • 對(duì)于離散型手勢(shì)UITapGestureRecognizer要么被識(shí)別,要么失敗,點(diǎn)按(假設(shè)點(diǎn)按次數(shù)設(shè)置為1,并且沒有添加長(zhǎng)按手勢(shì))下去一次不松開則此時(shí)什么也不會(huì)發(fā)生,松開手指立即識(shí)別并調(diào)用操作事件,并且狀態(tài)為3(已完成)。
  • 但是連續(xù)手勢(shì)要復(fù)雜一些,就拿旋轉(zhuǎn)手勢(shì)來說,如果兩個(gè)手指點(diǎn)下去不做任何操作,此時(shí)并不能識(shí)別手勢(shì)(因?yàn)槲覀冞€沒有旋轉(zhuǎn))但是其實(shí)已經(jīng)出發(fā)了觸摸開始事件,此時(shí)處于狀態(tài)0;如果此時(shí)旋轉(zhuǎn)會(huì)被識(shí)別,也就會(huì)調(diào)用對(duì)應(yīng)的操作事件,同時(shí)狀態(tài)變成1(手勢(shì)開始),但是狀態(tài)1只有一瞬間;緊接著變成狀態(tài)2(因?yàn)槲覀兊男D(zhuǎn)需要持續(xù)一會(huì)),并且重復(fù)調(diào)用操作事件(如果在事件中打印狀態(tài)會(huì)重復(fù)打印2);松開手指,此時(shí)狀態(tài)變?yōu)?,并調(diào)用1次操作事件。

為了大家更好的理解這個(gè)狀態(tài)的變化,不妨在操作事件中打印事件狀態(tài),會(huì)發(fā)現(xiàn)在操作事件中的狀態(tài)永遠(yuǎn)不可能為0(默認(rèn)狀態(tài)),因?yàn)橹灰{(diào)用此事件說明已經(jīng)被識(shí)別了。前面也說過,手勢(shì)識(shí)別從根本還是調(diào)用觸摸事件而完成的,連續(xù)手勢(shì)之所以會(huì)發(fā)生狀態(tài)轉(zhuǎn)換完全是由于觸摸事件中的移動(dòng)事件造成的,沒有移動(dòng)事件也就不存在這個(gè)過程中狀態(tài)變化。
大家通過蘋果官方的分析圖再理解一下上面說的內(nèi)容:


313E155C-7FD0-4685-A297-812FB3870CC4.png

手勢(shì)的具體使用這里就不贅述了,先來看一下下面幾種使用手勢(shì)時(shí)產(chǎn)生沖突的情況。

手勢(shì)和UIResponder之間的沖突

先看一個(gè)簡(jiǎn)單的例子:


BA814FD0-7F14-4BE1-A902-9C1C1F52BB6C.png

控制器的視圖上add了一個(gè)View記為YellowView,并綁定了一個(gè)單擊手勢(shì)識(shí)別器。

@interface ViewController ()
@property (weak, nonatomic) IBOutlet YellowView *yellowView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [self.yellowView addGestureRecognizer:tap];
}

- (void)tap:(id)sender {
    NSLog(@"tap");
}
@end

點(diǎn)擊YellowView,日志打印如下:

FAD8DBA7-D76E-4C4C-AA7A-A71CE1D648BA.png

從日志上看出YellowView最后cancel了對(duì)觸摸事件的響應(yīng),而正常應(yīng)當(dāng)是觸摸結(jié)束后,YellowView的touchesEnded:withEvent:方法被調(diào)用才對(duì)。另外,期間還執(zhí)行了手勢(shì)識(shí)別器綁定的action。對(duì)于這種現(xiàn)象,官方文檔上有這么一段描述:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled. The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties:

這段描述的意思是:UIWindow會(huì)先把touch事件分發(fā)給手勢(shì)識(shí)別器,然后再分發(fā)給hit-tested view,如果一個(gè)手勢(shì)識(shí)別器分析了這一系列的點(diǎn)擊事件之后沒有識(shí)別出該手勢(shì),hit-tested view將會(huì)接收完整的點(diǎn)擊事件。如果手勢(shì)識(shí)別器識(shí)別了該手勢(shì),hit-tested view將會(huì)取消這次點(diǎn)擊。由此可以看出:手勢(shì)識(shí)別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級(jí)

按照這個(gè)解釋,UIWindow在將事件傳遞給hit-tested view即YellowView之前,先傳遞給了手勢(shì)識(shí)別器。手勢(shì)識(shí)別器成功識(shí)別了該事件,通知application取消YellowView對(duì)事件的響應(yīng)。

然而看日志,卻是YellowView的touchesBegan:withEvent:先調(diào)用了,既然手勢(shì)識(shí)別器先響應(yīng),不應(yīng)該上面的action先執(zhí)行嗎?實(shí)際上這個(gè)認(rèn)知是錯(cuò)誤的。手勢(shì)識(shí)別器的action的調(diào)用時(shí)機(jī)并不是手勢(shì)識(shí)別器接收到事件的時(shí)機(jī),而是手勢(shì)識(shí)別器成功識(shí)別事件后的時(shí)機(jī),即手勢(shì)識(shí)別器的狀態(tài)變?yōu)閁IGestureRecognizerStateRecognized。要證明UIWindow先將事件傳遞給了手勢(shì)識(shí)別器,還是需要看手勢(shì)識(shí)別器中這四個(gè)熟悉的方法的調(diào)用結(jié)果。

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

不過不要誤會(huì),UIGestureRecognizer并不繼承于UIResponder類,他們只是方法名相同而已。
這樣,我們就可以自定義一個(gè)繼承自UITapGestureRecognizer的子類,重寫這四個(gè)方法,觀察事件分發(fā)的順序。上面的四個(gè)分發(fā)聲明在UIGestureRecognizerSubclass.h中,所以想要重寫的話需要引入頭文件#import <UIKit/UIGestureRecognizerSubclass.h>。

#import "TapGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@implementation TapGestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 這里需要調(diào)用一下父類的touchesBegan方法,否則事件會(huì)被攔截消耗掉
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s", __func__);
}

@end

點(diǎn)擊YellowView,輸出以下內(nèi)容:


F5295756-A948-4B7D-9F7E-EDB5A7B3B488.png

可以看到,確實(shí)是手勢(shì)識(shí)別器先接收到了事件,然后hit-tested view接收到事件。接著手勢(shì)識(shí)別器識(shí)別了手勢(shì),執(zhí)行action,再由Application取消了YellowView對(duì)事件的響應(yīng)。

那么UIWindow是怎么知道要把事件傳遞給哪些手勢(shì)識(shí)別器的呢?我們上面有一張圖提到過,這些手勢(shì)識(shí)別器的相關(guān)信息都儲(chǔ)存在UITouch對(duì)象的gestureRecognizers里面,這是一個(gè)數(shù)組,可以儲(chǔ)存多個(gè)手勢(shì)識(shí)別器。

UIGestureRecognizer分為離散型手勢(shì)和持續(xù)型手勢(shì),我們上面的demo用的是離散型手勢(shì),那么如果是持續(xù)型手勢(shì)又會(huì)有什么樣的結(jié)果呢?我們把UITapGestureRecognizer用UIPanGestureRecognizer替換,然后在YellowView上面執(zhí)行一次滑動(dòng),輸出結(jié)果如下:


4D526623-3469-45E1-9166-C49F74A7F581.png

在一開始滑動(dòng)的過程中,手勢(shì)識(shí)別器處在識(shí)別手勢(shì)階段,滑動(dòng)產(chǎn)生的連續(xù)事件既會(huì)傳遞給手勢(shì)識(shí)別器又會(huì)傳遞給YellowView,因此YellowView的touchesMoved:withEvent:在開始一段時(shí)間內(nèi)會(huì)持續(xù)調(diào)用;當(dāng)手勢(shì)識(shí)別器成功識(shí)別了該滑動(dòng)手勢(shì)時(shí),手勢(shì)識(shí)別器的action開始調(diào)用,同時(shí)通知Application取消YellowView對(duì)事件的響應(yīng)。之后僅由滑動(dòng)手勢(shì)識(shí)別器接收事件并響應(yīng),YellowView不再接收事件。另外,在滑動(dòng)的過程中,若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),則事件在觸摸滑動(dòng)過程中會(huì)一直傳遞給hit-tested view,直到觸摸結(jié)束。

手勢(shì)識(shí)別器的3個(gè)屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

在介紹這三個(gè)屬性之前,先來總結(jié)一下手勢(shì)識(shí)別器與UIResponder對(duì)于事件響應(yīng)的聯(lián)系:
當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時(shí),UIWindow都會(huì)傳遞事件尋求響應(yīng)。

  • UIWindow先將綁定了觸摸對(duì)象的事件傳遞給觸摸對(duì)象上綁定的手勢(shì)識(shí)別器,再發(fā)送給觸摸對(duì)象對(duì)應(yīng)的hit-tested view。
  • 手勢(shì)識(shí)別器識(shí)別手勢(shì)期間,若觸摸對(duì)象的觸摸狀態(tài)發(fā)生變化,事件都是先發(fā)送給手勢(shì)識(shí)別器再發(fā)送給hit-test view。
  • 手勢(shì)識(shí)別器若成功識(shí)別了手勢(shì),則通知Application取消hit-tested view對(duì)于事件的響應(yīng),并停止向其發(fā)送事件。
  • 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),而此時(shí)觸摸并未結(jié)束,則停止向手勢(shì)識(shí)別器發(fā)送事件,僅向hit-tested view發(fā)送事件。
  • 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),且此時(shí)觸摸已結(jié)束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對(duì)事件的響應(yīng)。

cancelsTouchInView

默認(rèn)為YES。表示當(dāng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)之后,會(huì)通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng),并不再傳遞事件給hit-tested view。若設(shè)置成NO,表示手勢(shì)識(shí)別成功后不取消響應(yīng)鏈對(duì)事件的響應(yīng),事件依舊會(huì)傳遞給hit-test view。把上面的demo中手勢(shì)的cancelsTouchInView屬性設(shè)置為NO,打印輸出結(jié)果:


2AFFDDBD-4425-40EE-882D-6DA7BC4F5138.png

可以看到,即便滑動(dòng)手勢(shì)識(shí)別器識(shí)別了手勢(shì),Application也會(huì)依舊發(fā)送事件給YellowView。

delaysTouchesBegan

默認(rèn)為NO。默認(rèn)情況下手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,當(dāng)觸摸狀態(tài)發(fā)生改變時(shí),Application都會(huì)將事件分別傳遞給手勢(shì)識(shí)別器和hit-tested view;若設(shè)置成YES,則表示手勢(shì)識(shí)別器再識(shí)別手勢(shì)期間,截?cái)嗍录床粫?huì)將事件發(fā)送給hit-tested view。把上面demo中的手勢(shì)識(shí)別器的delaysTouchesBegan設(shè)置為YES。


36D3BB79-B0A9-499D-8BEE-B857914B7062.png

因?yàn)榛瑒?dòng)手勢(shì)識(shí)別器在識(shí)別期間,事件不會(huì)傳遞給YellowView,因此期間YellowView的touchesBegan:withEvent:和touchesMoved:withEvent:都不會(huì)被調(diào)用。而后滑動(dòng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì),也就獨(dú)吞了事件,不會(huì)再傳遞給YellowView。因此只打印了手勢(shì)識(shí)別器成功識(shí)別手勢(shì)后的action調(diào)用。

delaysTouchesEnded

默認(rèn)為YES。當(dāng)手勢(shì)識(shí)別失敗時(shí),若此時(shí)觸摸已經(jīng)結(jié)束,會(huì)延遲一小段時(shí)間再調(diào)用響應(yīng)者的touchesEnded:withEvent:。若設(shè)置成NO,則在手勢(shì)識(shí)別失敗時(shí)會(huì)立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用touchesEnded:withEvent:結(jié)束事件響應(yīng)。

同一個(gè)視圖中的不同手勢(shì)之間的沖突

如果在同一個(gè)視圖上添加不同的手勢(shì)時(shí),也有可能會(huì)發(fā)生沖突。照例先上代碼:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self.imageView addGestureRecognizer:pan];

    UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    [self.imageView addGestureRecognizer:swipe];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        CGPoint point = [gestureRecognizer translationInView:self.imageView];
        self.imageView.transform = CGAffineTransformMakeTranslation(point.x, point.y);
    }else if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.3 animations:^{
            self.imageView.transform = CGAffineTransformIdentity;
        }];
    }
    NSLog(@"%s", __func__);
}

- (void)swipe:(UISwipeGestureRecognizer *)gestureRecognizer
{
    static BOOL flag = NO;
    self.imageView.image = flag ? [UIImage imageNamed:@"b4723daa4e8f8fc434bc2e79a1bc4d8c"] : [UIImage imageNamed:@"a96703f39203a4e650f9e24655dc4864"];
    flag = !flag;
    NSLog(@"%s", __func__);
}

@end

代碼很簡(jiǎn)單,最初我們的目的是在imageView上面添加一個(gè)拖動(dòng)手勢(shì),一個(gè)輕掃手勢(shì)。拖動(dòng)的時(shí)候改變圖片的位置,輕掃的時(shí)候切換圖片。


QQ20171205-111141.gif

D6004995-02E2-46CE-A53E-F686D6784DF6.png

可以看到,盡管我減小了輕掃的幅度,加快了速度,輕掃手勢(shì)依然沒有起作用,就是因?yàn)檩p掃和拖動(dòng)這兩個(gè)手勢(shì)起了沖突。沖突的原因很簡(jiǎn)單,拖動(dòng)手勢(shì)的操作事件是在手勢(shì)的開始狀態(tài)(狀態(tài)1)識(shí)別執(zhí)行的,而輕掃手勢(shì)的操作事件只有在手勢(shì)結(jié)束狀態(tài)(狀態(tài)3)才能執(zhí)行,因此輕掃手勢(shì)就作為了犧牲品沒有被正確識(shí)別。要解決這個(gè)沖突可以利用requireGestureRecognizerToFail:方法來完成,這個(gè)方法可以指定某個(gè)手勢(shì)執(zhí)行的前提是另一個(gè)手勢(shì)識(shí)別失敗。

這里我們把拖動(dòng)手勢(shì)設(shè)置為輕掃手勢(shì)識(shí)別失敗之后執(zhí)行,這樣一來我們手指輕輕滑動(dòng)時(shí)系統(tǒng)會(huì)優(yōu)先考慮輕掃手勢(shì),如果最后發(fā)現(xiàn)該操作不是輕掃,那么就會(huì)執(zhí)行拖動(dòng)。

只需要添加代碼:

[pan requireGestureRecognizerToFail:swipe];

運(yùn)行效果:


QQ20171205-135252.gif
不同視圖上的手勢(shì)沖突

在上面響應(yīng)者鏈的學(xué)習(xí)中,我們知道了UIResponder響應(yīng)事件的時(shí)候是有優(yōu)先級(jí)的,上層觸摸事件執(zhí)行后就不再向下傳播。默認(rèn)情況下手勢(shì)也是類似的,先識(shí)別的手勢(shì)會(huì)阻斷手勢(shì)識(shí)別操作繼續(xù)傳播。下面我們用代碼驗(yàn)證一下:

我們?cè)诳刂破鞯囊晥D上面添加一個(gè)黃色的子視圖,然后在黃色視圖上面添加一個(gè)自定義的滑動(dòng)手勢(shì),在控制器的view上面也添加一個(gè)自定義的滑動(dòng)手勢(shì)。在自定義的滑動(dòng)手勢(shì)里面重寫touchBegan:withEvent:這四個(gè)相關(guān)的方法.

@interface GestureRecognizer : UIPanGestureRecognizer
@property (nonatomic, strong) NSString *panName;
@end

@implementation GestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

@end

// ViewController

@interface ViewController ()
@property (weak, nonatomic) IBOutlet YellowView *yellowView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    GestureRecognizer *pan = [[GestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    pan.panName = @"第一個(gè)";
    [self.yellowView addGestureRecognizer:pan];

    GestureRecognizer *panBottom = [[GestureRecognizer alloc] initWithTarget:self action:@selector(panBottom:)];
    panBottom.panName = @"第二個(gè)";
    [self.view addGestureRecognizer:panBottom];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    NSLog(@"%s", __func__);
}

- (void)panBottom:(UIGestureRecognizer *)gestureRecognizer{
    NSLog(@"%s", __func__);
}

在黃色視圖上滑動(dòng),輸出以下結(jié)果:


ACB1FA17-AC4B-4B57-ACB1-84BBBF83221F.png

可以看到,在手勢(shì)識(shí)別期間,UIWindow會(huì)依次向兩個(gè)手勢(shì)識(shí)別器和hit-test view發(fā)送事件,而手勢(shì)識(shí)別成功后,UIWindow停止向控制器視圖上面添加的滑動(dòng)手勢(shì)發(fā)送事件,導(dǎo)致其action無法被調(diào)用,從而產(chǎn)生沖突。為什么停止接收事件的是第二個(gè)滑動(dòng)手勢(shì)呢?還記得我們上面提到過的UITouch里面的數(shù)組gestureRecognizers嗎,手勢(shì)識(shí)別的優(yōu)先級(jí)跟數(shù)組的數(shù)據(jù)是保持一致的,和響應(yīng)者鏈的響應(yīng)順序也有點(diǎn)類似。

4AE1F41A-62F1-4DB3-8CF4-D71F21AF2547.png

我們可以看到,第一個(gè)手勢(shì)儲(chǔ)存在數(shù)組的最前面,他的優(yōu)先級(jí)比較高,所以會(huì)首先被響應(yīng)。

那么如何讓兩個(gè)有層次關(guān)系并且都添加了手勢(shì)的控件都能正確識(shí)別手勢(shì)呢?答案就是利用手勢(shì)代理的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:代理方法。
蘋果官方是這么描述這個(gè)方法的:

Asks the delegate if two gesture recognizers should be allowed to recognize gestures simultaneously.
This method is called when recognition of a gesture by either gestureRecognizer or otherGestureRecognizer would block the other gesture recognizer from recognizing its gesture. Note that returning YES is guaranteed to allow simultaneous recognition; returning NO, on the other hand, is not guaranteed to prevent simultaneous recognition because the other gesture recognizer's delegate may return YES.

這個(gè)方法主要是為了詢問手勢(shì)的代理,是否允許兩個(gè)手勢(shì)識(shí)別器同時(shí)識(shí)別該手勢(shì)。返回YES可以確保允許同時(shí)識(shí)別手勢(shì),返回NO的話不能保證一定不能同時(shí)識(shí)別,因?yàn)槠渌謩?shì)的代理也有可能返回YES。

在上面demo的ViewController中遵循UIGestureRecognizerDelegate協(xié)議,設(shè)置第一個(gè)手勢(shì)的代理為self,添加如下代碼:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(nonnull UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

滑動(dòng)黃色視圖,輸出以下結(jié)果:


45EB01B5-9148-4B7A-B53B-D3FAA63BFD7B.png

可以看到兩個(gè)手勢(shì)的action同時(shí)被調(diào)用了。

UIControl

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton、UISegmentControl、UISwitch等控件都是UIControl的子類。值得注意的是,UIControl是UIView的子類,因此本身也具有UIResponder的屬性。UIControl是一個(gè)抽象基類,我們不能直接使用UIControl類來實(shí)例化控件,它只是為控件子類定義一些通用的接口,并提供一些基礎(chǔ)實(shí)現(xiàn)。

UIControl作為能夠響應(yīng)事件的控件,必然也需要待事件交互符合條件時(shí)才去響應(yīng),因此也會(huì)跟蹤事件發(fā)生的過程,不同于UIControl以及UIGestureRecognizer通過touches系列方法跟蹤,UIControl有其獨(dú)特的跟蹤方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

乍一看,這四個(gè)方法和UIResponder那四個(gè)方法幾乎吻合,只不過UIControl只能接收單點(diǎn)觸控,因此這四個(gè)方法的參數(shù)是單個(gè)的UITouch對(duì)象。這四個(gè)方法的智能也和UIResponder一致,用來跟蹤觸摸的開始、滑動(dòng)、結(jié)束、取消。不過,UIControl本身也是UIResponder,因此同樣有touches系列的4個(gè)方法。事實(shí)上,UIControl的Tracking系列方法是在touch系列方法內(nèi)部調(diào)用的。比如beginTrackingWithTouch是在touchesBegan方法內(nèi)部調(diào)用的。這個(gè)我們也可以驗(yàn)證:

自定義一個(gè)繼承于UIControl的子類,重寫beginTrackingWithTouch和touchesBegan:withEvent:方法。
代碼如下:

@implementation Control

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"進(jìn)入%s", __func__);
    [super touchesBegan:touches withEvent:event];
    NSLog(@"離開%s", __func__);
}

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
    return [super beginTrackingWithTouch:touch withEvent:event];
}

@end

點(diǎn)擊自定義的Control,輸出以下結(jié)果:


{2BE316C1-94A7-4C85-8CCD-68FBEB8170CE}.png

可以看出,touchesBegan方法調(diào)用了beginTrackingWithTouch方法。這也說明了另外一個(gè)問題,UIControl的touchesBegan方法的實(shí)現(xiàn)與UIResponder的touchesBegan方法是有區(qū)別的。

當(dāng)UIControl跟蹤事件的過程中,識(shí)別出事件交互符合響應(yīng)條件,就會(huì)觸發(fā)target-action進(jìn)行響應(yīng)。UIControl控件通過addTarget:action:forControlEvents:添加事件處理的target和action,當(dāng)事件發(fā)生時(shí),UIControl會(huì)調(diào)用sendAction:to:forEvent:來將event發(fā)送給UIApplication對(duì)象,再由UIApplication對(duì)象調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上。

C8509D7E-767F-45F8-9F4F-2012288348AA.png

因此,我們可以通過重寫UIControl的sendAction:to:forEvent:或sendAction:to:from:forEvent:自定義事件執(zhí)行的target及action。

If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.

另外,若不指定target,即addTarget:action:forControlEvents:時(shí)target傳空,那么當(dāng)事件發(fā)生時(shí),UIControl會(huì)在響應(yīng)鏈從上往下尋找定義了指定action方法的對(duì)象來響應(yīng)該action。

UIControl里面還有一個(gè)方法叫做sendActionsForControlEvents:這個(gè)方法的作用是發(fā)送與指定類型相關(guān)的所有行為消息。我們可以在任意位置(包括控件內(nèi)部和外部)調(diào)用控件的這個(gè)方法來發(fā)送參數(shù)controlEvents指定的消息。

UIControl、UIResponder、UIGestureRecognizer之間的優(yōu)先級(jí)關(guān)系

上面我們已經(jīng)分析過了,UIGestureRecognizer的優(yōu)先級(jí)是比UIResponder的優(yōu)先級(jí)高的,那么如果再加上一個(gè)UIControl呢?
我們先來比較一下UIControl和UIResponder之間的優(yōu)先級(jí)關(guān)系,這里的UIResponder我們用UIView來代替

首先如果UIControl添加在UIView上面的時(shí)候,毋庸置疑,UIControl會(huì)首先響應(yīng),參照button添加在視圖上。那么如果把UIView添加在UIControl上面的時(shí)候,誰會(huì)響應(yīng)事件呢?我們用代碼來驗(yàn)證一下:

自定義一個(gè)UIView,重寫touchesBegan:方法

@implementation YellowView

- (void)touchesBegan:(NSSet<UITouch *> *)touches   withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
}

@end

自定義一個(gè)UIControl,里面什么都不用寫。

在ViewController里面添加如下代碼:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Control *control = [[Control alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:control];

    YellowView *yellowView = [[YellowView alloc] initWithFrame:CGRectMake(10, 10, 50, 50)];
    yellowView.backgroundColor = [UIColor yellowColor];
    [control addSubview:yellowView];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}

@end

點(diǎn)擊上層的UIControl,輸出結(jié)果如下:


E0B59E0B-0468-46E4-91F4-0B723E2BDD95.png

可以看到UIControl的action并沒有響應(yīng)。看來自定義的UIControl與UIResponder之間的優(yōu)先級(jí)還是遵循響應(yīng)鏈的層級(jí)的,這就表示UIResponder和UIControl的優(yōu)先級(jí)是相同的,而UIGestureRecognizer的優(yōu)先級(jí)比UIControl高,由此推斷的話,UIGestureRecognizer的優(yōu)先級(jí)好像是比UIControl高的,具體是什么樣子的,我們還是來驗(yàn)證一下。

現(xiàn)在我們把層級(jí)改變一下,把UIControl添加到y(tǒng)ellowView上面,然后給yellowView添加一個(gè)tap手勢(shì),代碼如下:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    YellowView *blueView = [[YellowView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    blueView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:blueView];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [blueView addGestureRecognizer:tap];

    Control *control = [[Control alloc] initWithFrame:CGRectMake(10, 10, 50, 50)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [blueView addSubview:control];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}

- (void)tap:(UIGestureRecognizer *)gestureRecognizer
{
    NSLog(@"tap");
}

@end

這里為了讓我們的代碼更嚴(yán)謹(jǐn),我們把上面YellowView中重寫的touchesBegan方法注掉。現(xiàn)在點(diǎn)擊自定義的Control,打印結(jié)果如下:

1A500D7E-69C1-4BA4-B537-660B4D4F90D1.png

可以看到,系統(tǒng)執(zhí)行了手勢(shì)的action,并沒有執(zhí)行UIControl的action,這好像跟我們上面預(yù)測(cè)的手勢(shì)的優(yōu)先級(jí)比UIControl高是一致的。但是真的是這樣嗎?我們把自定義的UIControl替換成UIButton,其它地方不變,再點(diǎn)擊一次button,打印結(jié)果變成了這樣:


A4E97A1F-19D8-4B80-BCEB-625FC3EA2D1F.png

同樣都是繼承于UIControl,這control和control的差別咋就那么大捏???

別急,蘋果爸爸已經(jīng)給了我們合理的解釋:

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:

A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

在iOS6以后,默認(rèn)的control actions會(huì)阻止與該action操作相同的手勢(shì)的識(shí)別。例如:UIButton的默認(rèn)操作是單擊,如果你在這個(gè)button的父視圖上面添加了一個(gè)tap手勢(shì),用戶單擊button,系統(tǒng)會(huì)調(diào)用button的action而不是手勢(shì)的action。這種規(guī)則僅僅適用于手勢(shì)操作和UIcontrol的默認(rèn)操作相同的情況下,包含以下幾種情況:

  • 單擊:UIButton,UISwitch,UIStepper,UISegmentedControl和UIPageControl
  • 滑動(dòng):UISlider,滑動(dòng)方向與slider平行
  • 拖動(dòng):UISwitch,拖動(dòng)方向與switch平行

這里提到了兩點(diǎn),第一是手勢(shì)和UIControl的默認(rèn)操作相同,也就是說如果UIControl沒有默認(rèn)操作(比如我們自定義的UIControl)或者是默認(rèn)操作和添加的手勢(shì)不同,那么手勢(shì)識(shí)別器的識(shí)別優(yōu)先級(jí)高,UIControl不會(huì)阻止手勢(shì)識(shí)別。第二是在UIButton的父視圖上添加手勢(shì),如果你把一個(gè)添加了手勢(shì)的視圖添加在UIButton上面,那么UIButton是不能阻止該手勢(shì)識(shí)別的。這兩點(diǎn)讀者可以自行驗(yàn)證。

總結(jié):自定義的UIControl和UIResponder的優(yōu)先級(jí)相同,都比UIGestureRecognizer低,有默認(rèn)操作的UIControl會(huì)組織添加在父視圖上面的有相同操作的手勢(shì)的識(shí)別。

參考文章:
iOS觸摸事件全家桶
手勢(shì)識(shí)別(四)多手勢(shì)間的交互與共存
iOS觸摸事件傳遞響應(yīng)之被忽視的手勢(shì)識(shí)別器工作原理
iOS開發(fā)系列--觸摸事件、手勢(shì)識(shí)別、搖晃事件、耳機(jī)線控
UIKit: UIControl
iOS觸摸事件的流動(dòng)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,653評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,372評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,738評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,179評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評(píng)論 1 283
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,610評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,916評(píng)論 2 372

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

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點(diǎn)擊了?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 57,623評(píng)論 51 601
  • 在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,054評(píng)論 4 26
  • 簡(jiǎn)介 iOS 事件分為三大類 觸摸事件 加速器事件 遠(yuǎn)程控制事件 以下我們講解觸摸事件觸摸事件是我們平時(shí)遇到最多的...
    AKsoftware閱讀 22,548評(píng)論 23 72
  • iOS中的事件 用戶與app交互的時(shí)候會(huì)產(chǎn)生各種各樣的的事件,iOS中事件分為三大類型:1)觸摸事件 ;2)加速計(jì)...
    jason_Yun閱讀 715評(píng)論 0 3
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點(diǎn)觸摸的強(qiáng)大功能,喬布斯讓人們認(rèn)識(shí)到手機(jī)其實(shí)...
    翹楚iOS9閱讀 2,982評(píng)論 0 13