iOS 事件(UITouch、UIControl弄诲、UIGestureRecognizer)傳遞機(jī)制

一.觸摸、事件、響應(yīng)者

1. UITouch

源起觸摸

一個(gè)手指一次觸摸屏幕睁枕,就對應(yīng)生成一個(gè)UITouch對象。多個(gè)手指同時(shí)觸摸屏幕,生成多個(gè)UITouch對象外遇。

多個(gè)手指先后觸摸注簿,系統(tǒng)會根據(jù)觸摸的位置判斷是否更新同一個(gè)UITouch對象。若兩個(gè)手指一前一后觸摸同一個(gè)位置(即雙擊)跳仿,那么第一次觸摸時(shí)生成一個(gè)UITouch對象诡渴,第二次觸摸會更新這個(gè)UITouch對象,這是該UITouch對象的Tap Count屬性值從1變成2菲语,若兩個(gè)手指一前一后觸摸的位置不同妄辩,將會生成兩個(gè)UITouch對象,兩者之間沒有聯(lián)系山上。

每個(gè)UITouch對象記錄了觸摸的一些信息眼耀,包括觸摸時(shí)間、位置佩憾、階段哮伟、所處的視圖、窗口等信息妄帘。

//?觸摸的各個(gè)階段狀態(tài)?

//?例如當(dāng)手指移動時(shí)楞黄,會更新phase屬性到UITouchPhaseMoved;

//?手指離屏后抡驼,更新到UITouchPhaseEnded

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)

};復(fù)制代碼

手指離開屏幕一段時(shí)間后鬼廓,確定該UITouch對象不會再被更新,就釋放致盟。

2.UIEvent

事件的真身

觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng)碎税,一個(gè)觸摸事件對應(yīng)一個(gè)UIEvent對象,其中的type屬性標(biāo)識了事件的類型馏锡,事件有如下幾種類型:

typedef?NS_ENUM(NSInteger,?UIEventType)?{

????UIEventTypeTouches,

????UIEventTypeMotion,

????UIEventTypeRemoteControl,

????UIEventTypePresses?NS_ENUM_AVAILABLE_IOS(9_0),

};復(fù)制代碼

這里我們所說的事件具體指的是觸摸事件蚣录。

UIEvent對象中包含了觸發(fā)該對象的觸摸對象集合,因?yàn)橐粋€(gè)觸摸事件可能是由多個(gè)手指同時(shí)觸摸產(chǎn)生的眷篇。觸摸對象集合通過allTouches屬性獲取萎河。

3.UIResponder

UIResponder是iOS中用于處理用戶事件的API,可以處理觸摸事件蕉饼、按壓事件(3D touch)虐杯、遠(yuǎn)程控制事件、硬件運(yùn)動事件昧港∏嬉可以通過touchesBegan、pressesBegan创肥、motionBegan达舒、remoteControlReceivedWithEvent等方法值朋,獲取到對應(yīng)的回調(diào)消息。UIResponder不只用來接收事件巩搏,還可以處理和傳遞對應(yīng)的事件昨登,如果當(dāng)前響應(yīng)者不能處理,則轉(zhuǎn)發(fā)給其他合適的響應(yīng)者處理贯底。

應(yīng)用程序通過響應(yīng)者來接收和處理事件丰辣,響應(yīng)者可以是繼承自UIResponder的任何子類,例如UIView禽捆、UIViewController笙什、UIApplication等。當(dāng)事件來到時(shí)胚想,系統(tǒng)會將事件傳遞給合適的響應(yīng)者琐凭,并且將其成為第一響應(yīng)者。

第一響應(yīng)者未處理的事件浊服,將會在響應(yīng)者鏈中進(jìn)行傳遞统屈,傳遞規(guī)則由UIResponder的nextResponder決定,可以通過重寫該屬性來決定傳遞規(guī)則臼闻。當(dāng)一個(gè)事件到來時(shí),第一響應(yīng)者沒有接收消息囤采,則順著響應(yīng)者鏈向后傳遞述呐。

二.尋找事件的第一響應(yīng)者

App接收到觸摸事件后,會被放入當(dāng)前應(yīng)用程序的UIApplication維護(hù)的事件隊(duì)列中蕉毯。

由于事件一次只有一個(gè)乓搬,但是能夠響應(yīng)的事件的響應(yīng)者眾多,所以這就存在一個(gè)尋找第一響應(yīng)者的過程代虾。

1. 事件自下而上傳遞

查找第一響應(yīng)者時(shí)进肯,有兩個(gè)非常關(guān)鍵的API,查找第一響應(yīng)者就是通過不斷調(diào)用子視圖的這兩個(gè)API完成的棉磨。

調(diào)用方法江掩,獲取到被點(diǎn)擊的視圖,也就是第一響應(yīng)者乘瓤。

-?(UIView?*)hitTest:(CGPoint)point?withEvent:(UIEvent?*)event;復(fù)制代碼

hitTest:withEvent:方法內(nèi)部會通過調(diào)用pointInside:這個(gè)方法环形,來判斷點(diǎn)擊區(qū)域是否在視圖上,是則返回YES衙傀,不是則返回NO抬吟。

-?(BOOL)pointInside:(CGPoint)point?withEvent:(UIEvent?*)event;復(fù)制代碼

具體流程:

應(yīng)用程序接收到觸摸事件后,將事件放入U(xiǎn)IApplication的事件隊(duì)列统抬,等到處理該事件時(shí)火本,將該事件出隊(duì)列危队,UIApplication將事件傳遞給窗口對象(UIWindow),如果存在多個(gè)窗口钙畔,則優(yōu)先詢問后顯示的窗口

如果窗口UIWindow不能響應(yīng)事件茫陆,則將事件傳遞給其他窗口;若窗口能響應(yīng)事件刃鳄,則從后往前詢問窗口的子視圖盅弛。

以此類推,如果視圖不能響應(yīng)事件叔锐,則將事件傳遞給同級的上一個(gè)子視圖挪鹏;如果能響應(yīng),就從后往前遍歷當(dāng)前視圖的子視圖愉烙。

如果當(dāng)前視圖的子視圖都不能響應(yīng)事件讨盒,則當(dāng)前視圖就是最合適的響應(yīng)者。

舉個(gè)例子:

如圖所示:


視圖層級如下(同一層級的視圖越在下面步责,表示越后添加):

現(xiàn)在假設(shè)在E視圖所處的屏幕位置觸發(fā)一個(gè)觸摸返顺,應(yīng)用接收到這個(gè)觸摸事件事件后,先將事件傳遞給UIWindow蔓肯,然后自下而上開始在子視圖中尋找第一響應(yīng)者遂鹊。事件傳遞的順序如下所示:

UIWindow將事件傳遞給UIViewController的視圖UIView,UIView判斷自身能響應(yīng)事件,將事件傳遞給子視圖A

A判斷自身能響應(yīng)該事件蔗包,繼續(xù)將事件傳遞給C(因?yàn)橐晥DC比視圖B后添加秉扑,因此優(yōu)先傳給C)。

C判斷自身能響應(yīng)事件调限,繼續(xù)將事件傳遞給F(同理F比E后添加)舟陆。

F判斷自身不能響應(yīng)事件,C又將事件傳遞給E耻矮。

E判斷自身能響應(yīng)事件秦躯,同時(shí)E已經(jīng)沒有子視圖,因此最終E就是第一響應(yīng)者裆装。

2. hitTest函數(shù)本質(zhì)

上面講到了事件在響應(yīng)者之間傳遞的規(guī)則踱承,視圖通過判斷自身能否響應(yīng)事件來決定是否繼續(xù)想子視圖傳遞。

這里涉及到兩個(gè)問題:

視圖判斷自身能否響應(yīng)事件的判斷依據(jù)是什么哨免?

如果能響應(yīng)勾扭,視圖是如何將事件傳遞給子視圖的?

針對第一個(gè)問題:

首先我們要知道铁瞒,以下幾種狀態(tài)的視圖是無法響應(yīng)事件的:

不允許交互:userInteractionEnabled = NO

隱藏:hidden = YES 如果父視圖隱藏妙色,那么子視圖也會隱藏,隱藏的視圖無法接收事件

透明度:alpha < 0.01 如果設(shè)置一個(gè)視圖的透明度<0.01慧耍,會直接影響子視圖的透明度身辨。alpha:0.0~0.01為透明丐谋。

其次,如果當(dāng)前視圖可以響應(yīng)事件煌珊,還必須通過pointInside函數(shù)判斷号俐,觸摸點(diǎn)是否在當(dāng)前視圖的坐標(biāo)范圍內(nèi),如果不在當(dāng)前視圖的坐標(biāo)范圍內(nèi)定庵,則無法響應(yīng)吏饿,如果在坐標(biāo)范圍內(nèi),并且該視圖可以響應(yīng)事件蔬浙,就進(jìn)入下一步事件的傳遞猪落。

針對第二個(gè)問題:

hitTest:withEvent: 方法返回一個(gè)UIView對象,作為當(dāng)前視圖層次中的響應(yīng)者畴博。默認(rèn)實(shí)現(xiàn)是:

若當(dāng)前視圖無法響應(yīng)事件笨忌,則返回nil

若當(dāng)前視圖可以響應(yīng)事件,但無子視圖可以響應(yīng)事件俱病,則返回自身作為當(dāng)前視圖層次中的事件響應(yīng)者

若當(dāng)前視圖可以響應(yīng)事件官疲,同時(shí)有子視圖可以響應(yīng),則從后往前遍歷子視圖亮隙,返回子視圖層次中的事件響應(yīng)者

以此類推途凫,直到找到的當(dāng)前視圖可以響應(yīng)事件,并且當(dāng)前視圖沒有子視圖溢吻,那么當(dāng)前視圖就是第一響應(yīng)者维费。

依據(jù)以上的描述我們可以推測出hitTest:WithEvent:的默認(rèn)實(shí)現(xiàn)大致如下:

-?(UIView?*)hitTest:(CGPoint)point?withEvent:(UIEvent?*)event{

????//3種狀態(tài)無法響應(yīng)事件

?????if?(self.userInteractionEnabled?==?NO?||?self.hidden?==?YES?||??self.alpha?<=?0.01)?return?nil;?

????//觸摸點(diǎn)若不在當(dāng)前視圖上則無法響應(yīng)事件

????if?([self?pointInside:point?withEvent:event]?==?NO)?return?nil;?

????//從后往前遍歷子視圖數(shù)組?

????int?count?=?(int)self.subviews.count;?

????for?(int?i?=?count?-?1;?i?>=?0;?i--)?

????{?

????????//?獲取子視圖

????????UIView?*childView?=?self.subviews[i];?

????????//?坐標(biāo)系的轉(zhuǎn)換,把觸摸點(diǎn)在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)

????????CGPoint?childP?=?[self?convertPoint:point?toView:childView];?

????????//詢問子視圖層級中的最佳響應(yīng)視圖

????????UIView?*fitView?=?[childView?hitTest:childP?withEvent:event];?

????????if?(fitView)?

????????{

????????????//如果子視圖中有更合適的就返回

????????????return?fitView;?

????????}

????}?

????//沒有在子視圖中找到更合適的響應(yīng)視圖,那么自身就是最合適的

????return?self;

}復(fù)制代碼

我們分別在上述示例的視圖層次中的每個(gè)視圖實(shí)現(xiàn)文件添加如下方法:

#pragma?mark?--------------------------?Override?Methods

-?(void)touchesBegan:(NSSet?*)touches?withEvent:(UIEvent?*)event?{

????NSLog(@"%s",__func__);

????[super?touchesBegan:touches?withEvent:event];

}

-?(void)touchesMoved:(NSSet?*)touches?withEvent:(UIEvent?*)event?{

????NSLog(@"%s",__func__);

????[super?touchesMoved:touches?withEvent:event];

}

-?(void)touchesEnded:(NSSet?*)touches?withEvent:(UIEvent?*)event?{

????NSLog(@"%s",__func__);

????[super?touchesEnded:touches?withEvent:event];

}

-?(void)touchesCancelled:(NSSet?*)touches?withEvent:(UIEvent?*)event?{

????NSLog(@"%s",__func__);

????[super?touchesCancelled:touches?withEvent:event];

}

-?(UIView?*)hitTest:(CGPoint)point?withEvent:(UIEvent?*)event{

????NSLog(@"%s",__func__);

????return?[super?hitTest:point?withEvent:event];

}

-?(BOOL)pointInside:(CGPoint)point?withEvent:(UIEvent?*)event{

????NSLog(@"%s",__func__);

????return?[super?pointInside:point?withEvent:event];

}復(fù)制代碼

然后單點(diǎn)E視圖煤裙,打印如下:

?-[AView?hitTest:withEvent:]

?-[AView?pointInside:withEvent:]

?-[CView?hitTest:withEvent:]

?-[CView?pointInside:withEvent:]

?-[FView?hitTest:withEvent:]

?-[FView?pointInside:withEvent:]

?-[EView?hitTest:withEvent:]

?-[EView?pointInside:withEvent:]

?-[AView?hitTest:withEvent:]

?-[AView?pointInside:withEvent:]

?-[CView?hitTest:withEvent:]

?-[CView?pointInside:withEvent:]

?-[FView?hitTest:withEvent:]

?-[FView?pointInside:withEvent:]

?-[EView?hitTest:withEvent:]

?-[EView?pointInside:withEvent:]

?-[EView?touchesBegan:withEvent:]

?-[CView?touchesBegan:withEvent:]

?-[AView?touchesBegan:withEvent:]

?-[EView?touchesEnded:withEvent:]

?-[CView?touchesEnded:withEvent:]

?-[AView?touchesEnded:withEvent:]復(fù)制代碼

從打印結(jié)果我們可以看到最終EView視圖先對事件進(jìn)行了響應(yīng)掩完,同時(shí)將事件沿著響應(yīng)鏈進(jìn)行傳遞噪漾。

以上打印結(jié)果我們會發(fā)現(xiàn)單機(jī)E視圖后硼砰,從[AView hitTest:withEvent:]到 [EView pointInside:withEvent:] 的過程會執(zhí)行兩遍,這個(gè)問題我查找了一些資料欣硼,但都沒有好的答案题翰,蘋果那邊的回復(fù)是這樣的:

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

具體詳見:https://lists.apple.com/archives/cocoa-dev/2014/Feb/msg00118.html

意思就是說hitTest是一個(gè)沒有副作用的純函數(shù),進(jìn)行多次調(diào)用也不會對外產(chǎn)生影響诈胜,因此系統(tǒng)可以多次調(diào)整調(diào)用之間被測試的點(diǎn)豹障。

這里并沒有給出具體的調(diào)用兩次的原因,你也可以理解為系統(tǒng)為了精確觸摸的點(diǎn)焦匈,而進(jìn)行了多次調(diào)用血公,但為什么是兩次,我也沒找到相關(guān)答案缓熟。

3.事件攔截

實(shí)際開發(fā)中我們經(jīng)常會遇到如下需求

事件攔截.gif

在Tabbar的Item上面添加提示視圖tipView累魔,當(dāng)點(diǎn)擊提示視圖tipview摔笤,對應(yīng)的Item也進(jìn)行響應(yīng),并且提示視圖tipView消失垦写。

很明顯吕世,這里的提示視圖tipView是添加在Tabbar上面的,但是提示視圖tipView的位置又超出了Tabbar的區(qū)域梯投,這時(shí)我們點(diǎn)擊提示視圖tipView命辖,會發(fā)現(xiàn)提示視圖tipView得不到響應(yīng)。

我們看一下調(diào)用的堆棧:



從堆棧中我們得出如下分析:

生成的觸摸事件首先傳到了UIWindow分蓖,然后UIWindow將事件傳遞給控制器的根視圖UILayoutContainerView,

UILayoutContainerView判斷自己可以響應(yīng)觸摸事件尔艇,然后將事件傳遞給子視圖Tabbar

子視圖Tabbar判斷觸摸點(diǎn)并不在自己的坐標(biāo)范圍內(nèi),因此返回nil咆疗,

這時(shí)UILayoutContainerView將事件傳遞其他子視圖UINavigationTransitionView,UINavigationTransitionView判斷自己可以響應(yīng)事件漓帚,就將事件時(shí)間傳遞給其子視圖UIViewControllerWrapperView

UIViewControllerWrapperView判斷自己可以響應(yīng)事件,就將事件傳遞給子視圖FJFFirstViewController控制器的View

FJFFirstViewController控制器的View判斷自己可以響應(yīng)事件午磁,然后就將事件傳遞給子視圖AView尝抖,AView判斷點(diǎn)擊位置不在自己的坐標(biāo)范圍,返回nil迅皇,所以FJFFirstViewController控制器的View就是第一響應(yīng)者昧辽。

從這邊的分析我們可以看出事件沒有傳遞到提示視圖tipView,在Tabbar這里就直接返回了登颓,因?yàn)門abbar判斷點(diǎn)擊位置不在自己的坐標(biāo)范圍內(nèi)搅荞。

因此我們需要做的就是修改Tabbar的hitTest:withEvent:函數(shù)里面判斷點(diǎn)擊位置是否在Tabbar坐標(biāo)范圍的的判斷條件,也就是需要重寫TabBard的 pointInside:withEvent:方法框咙,判斷如果當(dāng)前觸摸坐標(biāo)在子視圖tipView上面咕痛,就返回YES,否則返回NO喇嘱;這樣一來時(shí)間就會最終傳遞到tipView上面茉贡,最終事件就會由tipView來響應(yīng)。

代碼如下:

#import?"FJFTabbar.h"

@implementation?FJFTabbar

//TabBar

-?(BOOL)pointInside:(CGPoint)point?withEvent:(UIEvent?*)event?{

????//將觸摸點(diǎn)坐標(biāo)轉(zhuǎn)換到在CircleButton上的坐標(biāo)

????CGPoint?pointTemp?=?[self?convertPoint:point?toView:self.indicateView];

????//若觸摸點(diǎn)在CricleButton上則返回YES

????if?([self.indicateView?pointInside:pointTemp?withEvent:event])?{

????????return?YES;

????}

????//否則返回默認(rèn)的操作

????return?[super?pointInside:point?withEvent:event];

}

@end復(fù)制代碼

三.事件的響應(yīng)及傳遞

經(jīng)過Hit-Testing的過程后者铜,UIApplication已經(jīng)知道了第一響應(yīng)者是誰腔丧,接下來要做的事情就是:

將事件傳遞給第一響應(yīng)者

將事件沿著響應(yīng)鏈傳遞

A. 將事件傳遞給第一響應(yīng)者:

由于第一響應(yīng)者具有處理事件的最高優(yōu)先級,因此UIApplication會先將事件傳遞給它供其處理作烟。首先愉粤,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view拿撩,即第一響應(yīng)者衣厘。過程如下:

UIApplication?——>?UIWindow?——>?hit-tested?view復(fù)制代碼

以點(diǎn)擊EView視圖為例,在EView的 touchesBegan:withEvent:上斷點(diǎn)查看調(diào)用棧就能看清這一過程:


從這調(diào)用堆棧我們可以看出压恒,UIApplication對于將事件傳遞給那個(gè)UIWindow是很明確的影暴,UIWindow對于將事件傳遞給哪個(gè)視圖也是很明確的怖亭。因?yàn)檫@些信息都放在了UIEvent的Touch事件里面。

但是這些信息又是什么時(shí)候放入到UIEvent內(nèi)部的呢坤检?

可想而知因?yàn)镠it-Testing和SendEvent兩者中的UIEvent是同一個(gè)UIEvent,所以這應(yīng)該是在Hit-Testing尋找第一響應(yīng)者的過程中兴猩,填入U(xiǎn)IEvent內(nèi)部的。

B.將事件沿著響應(yīng)鏈傳遞:

因?yàn)槊總€(gè)響應(yīng)者必定都是UIResponder對象早歇,通過4個(gè)響應(yīng)觸摸事件的方法來響應(yīng)事件倾芝。每個(gè)UIResponder對象默認(rèn)都已經(jīng)實(shí)現(xiàn)了這4個(gè)方法,但是默認(rèn)不對觸摸事件做任何處理箭跳,單純只是將事件沿著響應(yīng)鏈傳遞晨另。若要截獲事件進(jìn)行自定義的響應(yīng)操作,就要重寫相關(guān)的方法谱姓。

第一響應(yīng)者接收到觸摸事件后借尿,就具有對觸摸事件的處理權(quán),它可以選擇自己處理這個(gè)事件屉来,也可以將這個(gè)事件沿著響應(yīng)鏈傳遞給下一個(gè)響應(yīng)者路翻,這個(gè)由響應(yīng)者之間構(gòu)成的視圖鏈就稱之為響應(yīng)鏈。

需要注意的是茄靠,上一節(jié)所說的事件傳遞的目的是為尋找事件的最佳響應(yīng)者茂契,是自下而上的傳遞;這里的事件傳遞目的是響應(yīng)者做出對事件的響應(yīng)慨绳,這個(gè)過程是自上而下的掉冶。前者為“尋找”,后者為“響應(yīng)”脐雪。

響應(yīng)者對于事件的操作方式:

響應(yīng)者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent:方法控制的厌小,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞。

響應(yīng)者對于接收到的事件有3種操作:

不攔截战秋,默認(rèn)操作

事件會自動沿著默認(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)者對象(UIResponder對象)都有一個(gè)nextResponder方法拐袜,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個(gè)響應(yīng)者吉嚣。因此,一旦事件的第一響應(yīng)者確定了蹬铺,這個(gè)事件所處的響應(yīng)鏈就確定了尝哆。

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

UIView

若視圖是控制器的根視圖甜攀,則其nextResponder為控制器對象秋泄;否則琐馆,其nextResponder為父視圖。

UIViewController

若控制器的視圖是window的根視圖恒序,則其nextResponder為窗口對象瘦麸;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller歧胁。

UIWindow

nextResponder為UIApplication對象滋饲。

UIApplication

若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對象,且不是UIView喊巍、UIViewController或app本身屠缭,則UIApplication的nextResponder為app delegate。

舉個(gè)例子:

事件響應(yīng)示例.png


如上圖所示崭参,響應(yīng)者鏈如下:

如果點(diǎn)擊UITextField后其會成為第一響應(yīng)者呵曹。

如果textField未處理事件,則會將事件傳遞給下一級響應(yīng)者鏈何暮,也就是其父視圖奄喂。

父視圖未處理事件則繼續(xù)向下傳遞,也就是UIViewController的View海洼。

如果控制器的View未處理事件砍聊,則會交給控制器處理。

控制器未處理則會交給UIWindow贰军。

然后會交給UIApplication玻蝌。

最后交給UIApplicationDelegate,如果其未處理則丟棄事件词疼。

UITextField?——>?UIView?——>?UIView?——>?UIViewController

?——>?UIWindow?——>?UIApplication?——>?UIApplicationDelegation復(fù)制代碼

圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的俯树,則其nextResponder為UIViewController對象;若是直接add在UIWindow上的贰盗,則其nextResponder為UIWindow對象许饿。

可以用以下方式打印一個(gè)響應(yīng)鏈中的每一個(gè)響應(yīng)對象,在第一響應(yīng)者的 touchBegin:withEvent: 方法中調(diào)用即可(別忘了調(diào)用父類的方法)

-?(void)printResponderChain?{

????UIResponder?*responder?=?self;

????printf("%s",[NSStringFromClass([responder?class])?UTF8String]);

????while?(responder.nextResponder)?{

????????responder?=?responder.nextResponder;

????????printf("?-->?%s",[NSStringFromClass([responder?class])?UTF8String]);

????}

}復(fù)制代碼

以點(diǎn)擊EView為例舵盈,重寫EView的touch Begin:WithEvent:

-?(void)touchesBegan:(NSSet?*)touches?withEvent:(UIEvent?*)event?{

????NSLog(@"%s",__func__);

????[self?printResponderChain];

????[super?touchesBegan:touches?withEvent:event];

}復(fù)制代碼

響應(yīng)鏈如下:

EView?-->?CView?-->?AView?-->?UIView?-->?FJFFirstViewController?-->?

UIViewControllerWrapperView?-->?UINavigationTransitionView?-->?

UILayoutContainerView?-->?UINavigationController?-->?

UIViewControllerWrapperView?-->?UITransitionView?-->?

UILayoutContainerView?-->?FJFTabBarViewController?-->?FJFWindow?-->?

FJFApplication?-->?AppDelegate復(fù)制代碼

另外如果有需要陋率,完全可以重寫響應(yīng)者的 nextResponder 方法來自定義響應(yīng)鏈。

四.UIGestureRecognizer秽晚、UIControl

上面我們講述了UIResponder響應(yīng)觸摸事件的過程瓦糟,但除了UIResponder之外,UIGestureRecognizer赴蝇、UIControl同樣具備對事件的處理能力菩浙。

以下將通過結(jié)合具體的示例來講解UIGestureRecognizer和UIControl是如何處理觸摸事件的。?

舉個(gè)例子:


代碼:

#pragma?mark?--------------------------?Life?Circle

-?(void)viewDidLoad?{

????[super?viewDidLoad];


????self.title?=?@"分類";


????//?view?tap

????FJFTapView?*tmpContainerView?=?[[FJFTapView?alloc]?initWithFrame:CGRectMake(50,?80,?260,?300)];

????tmpContainerView.backgroundColor?=?[UIColor?redColor];

????FJFTapGestureRecognizer?*tapGesture?=?[[FJFTapGestureRecognizer?alloc]?initWithTarget:self?action:@selector(viewTap:)];

????[tmpContainerView?addGestureRecognizer:tapGesture];

????[self.view?addSubview:tmpContainerView];


????//?view?longPress

????FJFLongPressView?*tmpLongPressView?=?[[FJFLongPressView?alloc]?initWithFrame:CGRectMake(50,?400,?260,?200)];

????tmpLongPressView.backgroundColor?=?[UIColor?grayColor];

????FJFLongPressGestureRecognizer?*longPressGesture?=?[[FJFLongPressGestureRecognizer?alloc]?initWithTarget:self?action:@selector(viewlongPress:)];

????[tmpLongPressView?addGestureRecognizer:longPressGesture];

????[self.view?addSubview:tmpLongPressView];


????//?button

????FJFButton?*tmpButton?=?[[FJFButton?alloc]?initWithFrame:CGRectMake(100,?50,?120,?80)];

????tmpButton.backgroundColor?=?[UIColor?greenColor];

????[tmpButton?setTitle:@"UIButton"?forState:UIControlStateNormal];

????[tmpButton?setTitleColor:[UIColor?blackColor]?forState:UIControlStateNormal];

????[tmpButton?addTarget:self?action:@selector(tmpButtonClicked:)?forControlEvents:UIControlEventTouchUpInside];

????[tmpContainerView?addSubview:tmpButton];


????//?imageControl

????FJFImageControl?*imageControl?=?[[FJFImageControl?alloc]?initWithFrame:CGRectMake(100,?150,?120,?80)?title:@"imageControl"?iconImageName:@"ic_red_box.png"];

????imageControl.backgroundColor?=?[UIColor?blueColor];

????[imageControl?addTarget:self?action:@selector(imageControlTouch:)?forControlEvents:UIControlEventTouchUpInside];

????[tmpContainerView?addSubview:imageControl];

}

#pragma?mark?--------------------------?Response?Event

//?tap

-?(void)viewTap:(UITapGestureRecognizer?*)tap?{

????NSLog(@"%s",?__FUNCTION__);

}

//?longPress

-?(void)viewlongPress:(UILongPressGestureRecognizer?*)longPress?{

????NSLog(@"%s",?__FUNCTION__);

}

//?buttonClicked

-?(void)tmpButtonClicked:(UIButton?*)sender?{

????NSLog(@"%s",?__FUNCTION__);

}

//?controlTouch

-?(void)imageControlTouch:(FJFImageControl?*)imageControl?{

?????NSLog(@"%s",?__FUNCTION__);

}復(fù)制代碼

如代碼所示:

FJFTapView 添加了繼承自UITapGestureRecognizer的FJFTapGestureRecognizer 單擊手勢

FJFLongPressView 添加了繼承自UILongPressGestureRecognizer的FJFLongPressGestureRecognizer 長按手勢

UIButton 添加 點(diǎn)擊事件

FJFImageControl 繼承自UIControl,也添加了點(diǎn)擊事件劲蜻,且UIButton和FJFImageControl都是FJFTapView的子視圖陆淀。

觀察各種情況的日志:

1.點(diǎn)擊FJFTapView:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFTapView?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]

[FJFTapView?touchesCancelled:withEvent:]復(fù)制代碼

2.長按FJFLongPressView:

[FJFLongPressGestureRecognizer?touchesBegan:withEvent:]

[FJFLongPressView?touchesBegan:withEvent:]

[FJFThreeViewController?viewlongPress:]

[FJFLongPressView?touchesCancelled:withEvent:]

[FJFLongPressGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewlongPress:]復(fù)制代碼

3.點(diǎn)擊UIButton:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFButton?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFButton?touchesEnded:withEvent:]

[FJFThreeViewController?tmpButtonClicked:]復(fù)制代碼

4.點(diǎn)擊FJFImageControl:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFImageControl?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]

[FJFImageControl?touchesCancelled:withEvent:]復(fù)制代碼

接下來我們一一解釋這些現(xiàn)象:

1. UIGestureRecognizer:

手勢分為離散型手勢(discrete gestures)和持續(xù)型手勢(continuous gesture)。系統(tǒng)提供的離散型手勢包括點(diǎn)按手勢([UITapGestureRecognizer](apple-reference-documentation://hcmEtJ0eLp))和輕掃手勢([UISwipeGestureRecognizer](apple-reference-documentation://hcKMJKvz5T))先嬉,其余均為持續(xù)型手勢轧苫。

兩者主要區(qū)別在于狀態(tài)變化過程:

離散型:

????識別成功:Possible?—>?Recognized

????識別失敗:Possible?—>?Failed復(fù)制代碼

持續(xù)型:

????完整識別:Possible?—>?Began?—>?[Changed]?—>?Ended

????不完整識別:Possible?—>?Began?—>?[Changed]?—>?Cancel復(fù)制代碼

A. 離散型手勢

從點(diǎn)擊FJFTapView的日志可以分析:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFTapView?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]

[FJFTapView?touchesCancelled:withEvent:]復(fù)制代碼

UIWindow在將事件傳遞給第一響應(yīng)者FJFTapView之前疫蔓,先將事件傳遞給相關(guān)的手勢識別器FJFTapGestureRecognizer浸剩,

若手勢成功識別事件,就會取消第一響應(yīng)者FJFTapView對事件的響

應(yīng)鳄袍;

若手勢沒能識別事件绢要,第一響應(yīng)者FJFTapView就會接手事件的處理。

這里我們可以得出:UIGestureRecognizer比UIResponder具有更高的事件響應(yīng)的優(yōu)先級

這個(gè)結(jié)論我們也可以從官方文檔中得出:

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.

還有一點(diǎn)需要注意的是:

UIGestureRecognizer對事件的響應(yīng)也是通過touch相關(guān)的4個(gè)方法來實(shí)現(xiàn)的拗小,而這4個(gè)方法聲明在UIGestureRecognizerSubclass.h中重罪。


而這里UIWindow之所以知道要把事件傳遞給哪些手勢識別器,主要還是通過UIEvent里面的gestureRecognizers數(shù)組來獲取的哀九,而數(shù)組里面的手勢識別器是在Hit-Test View尋找第一響應(yīng)者過程中填充的剿配。


這里UIWindow會取出UIEvent里面的gestureRecognizers數(shù)組的手勢識別器,將事件傳遞給各個(gè)手勢識別器阅束,如果有一個(gè)手勢識別器識別了事件呼胚,其他的手勢識別器就不會響應(yīng)該事件。

注意:這里取出gestureRecognizers數(shù)組的手勢識別器息裸,沒有按照特定的順序蝇更,比如說從前往后或是從后往前,可以通過hook掉UIGestureRecognizer的touch相關(guān)方法,去追蹤得出呼盆。

因此我們可以分析日志:

UIWindow 先將事件傳遞給gestureRecognizers數(shù)組里的手勢識別器年扩,然后再傳遞給第一響應(yīng)者FJFTapView.

因?yàn)槭謩葑R別器識別事件,需要一定時(shí)間访圃,因此FJFTapView先調(diào)用了touchesBegan厨幻,這是因?yàn)镕JFTapGestureRecognizer成功識別了事件,UIApplication就會取消FJFTapView對事件的響應(yīng)腿时。

B. 持續(xù)型手勢

從點(diǎn)擊FJFLongPressView日志分析:

[FJFLongPressGestureRecognizer?touchesBegan:withEvent:]

[FJFLongPressView?touchesBegan:withEvent:]

[FJFThreeViewController?viewlongPress:]

[FJFLongPressView?touchesCancelled:withEvent:]

[FJFLongPressGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewlongPress:]復(fù)制代碼

從日志我們可以看出長按手勢回調(diào)了兩次,我們通過分析兩次調(diào)用的堆棧:

第一次調(diào)用堆棧:


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


我們可以看出第一次調(diào)用是在runloop中通知監(jiān)聽的手勢識別器的觀察者况脆,來通知長按手勢識別器對長按事件進(jìn)行響應(yīng),此時(shí)手勢識別器的state為UIGestureRecognizerStateBegan批糟。

第二次調(diào)用是UIWindow 先將事件傳遞給UIEvent的gestureRecognizers數(shù)組里的手勢識別器格了,然后長按手勢識別器FJFLongPressGestureRecognizer識別成功進(jìn)行回調(diào),此時(shí)手勢識別器的state為UIGestureRecognizerStateEnded。

這里的調(diào)用邏輯其實(shí)跟單擊手勢識別器FJFTapGestureRecognizer相似跃赚,主要區(qū)別在于長按手勢識別器FJFLongPressGestureRecognizer調(diào)用了兩次笆搓。

C. 總結(jié)

當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時(shí),UIWindow都會傳遞事件尋求響應(yīng)纬傲。

-UIWindow先將觸摸事件傳遞給響應(yīng)鏈上綁定的手勢識別器满败,再發(fā)送給觸摸對象對應(yīng)的第一響應(yīng)者。

手勢識別器識別手勢期間叹括,若觸摸對象的觸摸狀態(tài)發(fā)生變化算墨,事件都是先發(fā)送給手勢識別器,再發(fā)送給第一響應(yīng)者汁雷。

手勢識別器如果成功識別手勢净嘀,則通知UIApplication取消第一響應(yīng)者對于事件的響應(yīng),并停止向第一響應(yīng)者發(fā)送事件侠讯。

如果手勢識別器未能識別手勢挖藏,而此時(shí)觸摸并未結(jié)束,則停止向手勢識別器發(fā)送事件厢漩,僅向第一響應(yīng)者發(fā)送事件膜眠。

如果手勢識別器未能識別手勢,且此時(shí)觸摸已經(jīng)結(jié)束溜嗜,則向第一響應(yīng)者發(fā)送end狀態(tài)的touch事件宵膨,以停止對事件的響應(yīng)。

D. 拓展

手勢識別器的3個(gè)屬性:

@property(nonatomic)?BOOL?cancelsTouchesInView;

@property(nonatomic)?BOOL?delaysTouchesBegan;

@property(nonatomic)?BOOL?delaysTouchesEnded;復(fù)制代碼

a. cancelsTouchesInView:

默認(rèn)為YES炸宵。表示當(dāng)手勢識別器成功識別了手勢之后辟躏,會通知Application取消響應(yīng)鏈對事件的響應(yīng),并不再傳遞事件給第一響應(yīng)者土全。若設(shè)置成NO捎琐,表示手勢識別成功后不取消響應(yīng)鏈對事件的響應(yīng),事件依舊會傳遞給第一響應(yīng)者裹匙。

以點(diǎn)擊FJFTapView為例野哭,將tapGesture.cancelsTouchesInView = NO;輸出日志如下:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFTapView?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]

[FJFTapView?touchesEnded:withEvent:]復(fù)制代碼

從日志我們可以看出,即便FJFTapGestureRecognizer識別了點(diǎn)擊手勢幻件,UIApplication也依舊將事件發(fā)送給FJFTapView.

b. delaysTouchesBegan:

默認(rèn)為NO拨黔。默認(rèn)情況下手勢識別器在識別手勢期間,當(dāng)觸摸狀態(tài)發(fā)生改變時(shí)绰沥,Application都會將事件傳遞給手勢識別器和第一響應(yīng)者篱蝇;若設(shè)置成YES,則表示手勢識別器在識別手勢期間徽曲,截?cái)嗍录憬兀床粫⑹录l(fā)送給第一響應(yīng)者。

以點(diǎn)擊FJFTapView為例秃臣,將tapGesture.delaysTouchesBegan = YES;輸出日志如下:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]復(fù)制代碼

從日志可以看出涧衙,手勢識別器識別手勢期間哪工,事件不會傳遞給FJFTapView,因此FJFTapView的touchesBegan:withEvent:不會被調(diào)用弧哎;而手勢識別器成功識別手勢后雁比,獨(dú)吞了事件酗洒,不會再傳遞給FJFTapView,因此只打印手勢識別器識別成功后手勢的綁定函數(shù)钦幔。

c. delaysTouchesEnded:

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

2.UIControl

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件丈牢,iOS中UIButton、UISegmentedControl瞄沙、UISwitch等控件都是UIControl的子類赡麦。

值得注意的是,UIConotrol是UIView的子類帕识,因此本身也具備UIResponder應(yīng)有的身份泛粹。

UIControl作為控件類的基類,它是一個(gè)抽象基類肮疗,我們不能直接使用UIControl類來實(shí)例化控件晶姊,它只是為控件子類定義一些通用的接口,并提供一些基礎(chǔ)實(shí)現(xiàn)伪货,以在事件發(fā)生時(shí)们衙,預(yù)處理這些消息并將它們發(fā)送到指定目標(biāo)對象上。

關(guān)于UIControl碱呼,此處介紹兩點(diǎn):

target-action機(jī)制

觸摸事件優(yōu)先級

Target-Action機(jī)制

Target-action是一種設(shè)計(jì)模式蒙挑,直譯過來就是”目標(biāo)-行為”。當(dāng)我們通過代碼為一個(gè)按鈕添加一個(gè)點(diǎn)擊事件時(shí)愚臀,通常是如下處理:

[button?addTarget:self?action:@selector(tapButton:)?forControlEvents:UIControlEventTouchUpInside];復(fù)制代碼


注:圖片來源于官方文檔Cocoa Application Competencies for iOS – Target Action

即當(dāng)事件發(fā)生時(shí)忆蚀,事件會被發(fā)送到控件對象中,然后再由這個(gè)控件對象去觸發(fā)target對象上的action行為姑裂,來最終處理事件馋袜。因此,Target-Action機(jī)制由兩部分組成:即目標(biāo)對象Target和行為Selector舶斧。目標(biāo)對象指定最終處理事件的對象欣鳖,而行為Selector則是處理事件的方法。

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

-?(BOOL)beginTrackingWithTouch:(UITouch?*)touch?withEvent:(nullable?UIEvent?*)event?{

????NSLog(@"%s",__func__);

????return?YES;

}

-?(BOOL)continueTrackingWithTouch:(UITouch?*)touch?withEvent:(nullable?UIEvent?*)event?{

????NSLog(@"%s",__func__);

????return?YES;

}

-?(void)endTrackingWithTouch:(nullable?UITouch?*)touch?withEvent:(nullable?UIEvent?*)event?{

????NSLog(@"%s",__func__);

}

-?(void)cancelTrackingWithEvent:(nullable?UIEvent?*)event?{

????NSLog(@"%s",__func__);

}復(fù)制代碼

這4個(gè)方法和UIResponder的那4個(gè)方法幾乎吻合怀酷,只不過UIControl只能接收單點(diǎn)觸控稻爬,因此接收的參數(shù)是單個(gè)UITouch對象。這幾個(gè)方法的職能也和UIResponder一致胰坟,用來跟蹤觸摸的開始因篇、滑動泞辐、結(jié)束笔横、取消。不過咐吼,UIControl本身也是UIResponder吹缔,因此同樣有touches系列的4個(gè)方法。事實(shí)上锯茄,UIControl的 Tracking 系列方法是在touch 系列方法內(nèi)部調(diào)用的厢塘。比如 beginTrackingWithTouch ?是在 touchesBegan 方法內(nèi)部調(diào)用的, 因此它雖然也是UIResponder肌幽,但touches 系列方法的默認(rèn)實(shí)現(xiàn)和UIResponder本類還是有區(qū)別的晚碾。

我們來分析下FJFButton的日志輸出以及調(diào)用堆棧:

日志輸出:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFButton?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFButton?touchesEnded:withEvent:]

[FJFThreeViewController?tmpButtonClicked:]復(fù)制代碼

調(diào)用堆棧:


從以上信息,我們可以分析:

UIWindow 首先將事件傳遞給響應(yīng)鏈上綁定的手勢識別器FJFTapGestureRecognizer喂急,再傳遞給第一響應(yīng)者FJFButton

手勢識別器FJFTapGestureRecognizer和第一響應(yīng)者FJFButton分別調(diào)用touch相關(guān)方法對事件進(jìn)行識別格嘁,

最終第一響應(yīng)者FJFButton對事件進(jìn)行響應(yīng)調(diào)用 sendAction:to:forEvent:將target、action以及event對象發(fā)送給UIApplication廊移,UIApplication對象再通過 sendAction:to:from:forEvent:向target發(fā)送action糕簿。

通過這個(gè)結(jié)果,我們會疑問:UIControl比其父視圖上的手勢識別器具有更高的事件響應(yīng)優(yōu)先級?

接下來我們看下繼承自UIControl的FJFImageControl的日志和調(diào)用堆棧:

日志輸出:

[FJFTapGestureRecognizer?touchesBegan:withEvent:]

[FJFImageControl?touchesBegan:withEvent:]

[FJFTapGestureRecognizer?touchesEnded:withEvent:]

[FJFThreeViewController?viewTap:]

[FJFImageControl?touchesCancelled:withEvent:]復(fù)制代碼

調(diào)用堆棧:


從以上信息狡孔,我們又可以得出::UIControl比其父視圖上的手勢識別器的優(yōu)先級來的低懂诗?

經(jīng)驗(yàn)證系統(tǒng)提供的有默認(rèn)action操作的UIControl,例如UIbutton苗膝、UISwitch等的單擊殃恒,UIControl的響應(yīng)優(yōu)先級比手勢識別器高,而對于自定義的UIControl,響應(yīng)的優(yōu)先級比手勢低辱揭。

至于為什么會這樣芋类,沒找到具體原因,但測試的結(jié)果界阁,推測系統(tǒng)應(yīng)該是依據(jù)UITouch的touchIdentifier來進(jìn)行區(qū)別處理侯繁。

Target-Action的管理:

UIControl通過addTarget方法和removeTarget方法來添加和刪除Target-Action的操作。

//?添加

-?(void)addTarget:(id)target?action:(SEL)action?forControlEvents:(UIControlEvents)controlEvents

?//?刪除

-?(void)removeTarget:(id)target?action:(SEL)action?forControlEvents:(UIControlEvents)controlEvents復(fù)制代碼

如果想獲取控件對象所有相關(guān)的target對象泡躯,則可以調(diào)用allTargets方法贮竟,該方法返回一個(gè)集合丽焊。集合中可能包含NSNull對象,表示至少有一個(gè)nil目標(biāo)對象咕别。

而如果想獲取某個(gè)target對象及事件相關(guān)的所有action技健,則可以調(diào)用actionsForTarget:forControlEvent:方法。

不過惰拱,這些都是UIControl開放出來的接口雌贱。我們還是想要探究一下,UIControl是如何去管理Target-Action的呢偿短?

實(shí)際上欣孤,我們在程序某個(gè)合適的位置打個(gè)斷點(diǎn)來觀察UIControl的內(nèi)部結(jié)構(gòu),可以看到這樣的結(jié)果:


從圖中我們可以看出昔逗,UIControl內(nèi)部實(shí)際上是有一個(gè)可變數(shù)組(_targetActions)來保存Target-Action降传,數(shù)組中的每個(gè)元素是一個(gè)UIControlTargetAction對象。UIControlTargetAction類是一個(gè)私有類勾怒,內(nèi)部維護(hù)

@interface?UIControlTargetAction?:?NSObject?{

????SEL?_action;

????BOOL?_cancelled;

????unsigned?int?_eventMask;//?事件類型,比如:UIControlEventTouchUpInside

????id?_target;

}

這四個(gè)變量婆排,UIControl正是依據(jù)UIControlTargetAction來對事件進(jìn)行處理。

五.事件完整響應(yīng)鏈

系統(tǒng)通過 IOKit.framework來處理硬件操作笔链,其中屏幕處理也通過IOKit完成(IOKit可能是注冊監(jiān)聽了屏幕輸出的端口)

當(dāng)用戶操作屏幕段只,IOKit收到屏幕操作,會將這次操作封裝為IOHIDEvent對象鉴扫。通過mach port(IPC進(jìn)程間通信)將事件轉(zhuǎn)發(fā)給SpringBoard來處理赞枕。

SpringBoard是iOS系統(tǒng)的桌面程序。SpringBoard收到mach port發(fā)過來的事件幔妨,喚醒main runloop來處理鹦赎。

main runloop將事件交給source1處理,source1會調(diào)用__IOHIDEventSystemClientQueueCallback()函數(shù)误堡。

函數(shù)內(nèi)部會判斷古话,是否有程序在前臺顯示,如果有則通過mach port將IOHIDEvent事件轉(zhuǎn)發(fā)給這個(gè)程序锁施。

如果前臺沒有程序在顯示陪踩,則表明SpringBoard的桌面程序在前臺顯示,也就是用戶在桌面進(jìn)行了操作悉抵。

__IOHIDEventSystemClientQueueCallback()函數(shù)會將事件交給source0處理肩狂,source0會調(diào)用__UIApplicationHandleEventQueue()函數(shù),函數(shù)內(nèi)部會做具體的處理操作姥饰。

例如用戶點(diǎn)擊了某個(gè)應(yīng)用程序的icon傻谁,會將這個(gè)程序啟動。

應(yīng)用程序接收到SpringBoard傳來的消息列粪,會喚醒main runloop并將這個(gè)消息交給source1處理审磁,source1調(diào)用__IOHIDEventSystemClientQueueCallback()函數(shù)谈飒,在函數(shù)內(nèi)部會將事件交給source0處理,并調(diào)用source0的__UIApplicationHandleEventQueue()函數(shù)态蒂。

在__UIApplicationHandleEventQueue()函數(shù)中杭措,會將傳遞過來的IOHIDEvent轉(zhuǎn)換為UIEvent對象。

在函數(shù)內(nèi)部钾恢,將事件放入U(xiǎn)IApplication的事件隊(duì)列手素,等到處理該事件時(shí),將該事件出隊(duì)列瘩蚪,UIApplication將事件傳遞給窗口對象(UIWindow)泉懦,如果存在多個(gè)窗口,則從后往前詢問最上層顯示的窗口

窗口UIWindow通過hitTest和pointInside操作募舟,判斷是否可以響應(yīng)事件祠斧,如果窗口UIWindow不能響應(yīng)事件闻察,則將事件傳遞給其他窗口拱礁;若窗口能響應(yīng)事件,則從后往前詢問窗口的子視圖辕漂。

以此類推呢灶,如果當(dāng)前視圖不能響應(yīng)事件,則將事件傳遞給同級的上一個(gè)子視圖钉嘹;如果能響應(yīng)鸯乃,就從后往前遍歷當(dāng)前視圖的子視圖。

如果當(dāng)前視圖的子視圖都不能響應(yīng)事件跋涣,則當(dāng)前視圖就是第一響應(yīng)者缨睡。

找到第一響應(yīng)者,事件的傳遞的響應(yīng)鏈也就確定的陈辱。

如果第一響應(yīng)者非UIControl子類且響應(yīng)鏈上也沒有綁定手勢識別器UIGestureRecognizer;

那么由于第一響應(yīng)者具有處理事件的最高優(yōu)先級奖年,因此UIApplication會先將事件傳遞給它供其處理。首先沛贪,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window陋守,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view,即第一響應(yīng)者,第一響應(yīng)者具有對事件的完全處理權(quán)利赋,默認(rèn)對事件不進(jìn)行處理水评,傳遞給下一個(gè)響應(yīng)者(nextResponder);如果響應(yīng)鏈上的對象一直沒有處理該事件媚送,則最后會交給UIApplication中燥,如果UIApplication實(shí)現(xiàn)代理,會交給UIApplicationDelegate塘偎,如果UIApplicationDelegate沒處理疗涉,則該事件會被丟棄幽纷。

如果第一響應(yīng)者非UIControl子類但響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;

UIWindow會將事件先發(fā)送給響應(yīng)鏈上綁定的手勢識別器UIGestureRecognizer,再發(fā)送給第一響應(yīng)者博敬,如果手勢識別器能成功識別事件友浸,UIApplication默認(rèn)會向第一響應(yīng)者發(fā)送cancel響應(yīng)事件的命令;如果手勢識別器未能識別手勢,而此時(shí)觸摸并未結(jié)束偏窝,則停止向手勢識別器發(fā)送事件收恢,僅向第一響應(yīng)者發(fā)送事件。如果手勢識別器未能識別手勢祭往,且此時(shí)觸摸已經(jīng)結(jié)束伦意,則向第一響應(yīng)者發(fā)送end狀態(tài)的touch事件,以停止對事件的響應(yīng)硼补。

如果第一響應(yīng)者是自定義的UIControl的子類同時(shí)響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;這種情況跟第一響應(yīng)者非UIControl子類但響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer`處理邏輯一樣;

如果第一響應(yīng)者是UIControl的子類且是系統(tǒng)類(UIButton驮肉、UISwitch)同時(shí)響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;

UIWindow會將事件先發(fā)送給響應(yīng)鏈上綁定的手勢識別器UIGestureRecognizer,再發(fā)送給第一響應(yīng)者已骇,如果第一響應(yīng)者能響應(yīng)事件离钝,UIControl調(diào)用調(diào)用sendAction:to:forEvent:將target、action以及event對象發(fā)送給UIApplication褪储,UIApplication對象再通過 sendAction:to:from:forEvent:向target發(fā)送action卵渴。

六. 延伸閱讀

iOS事件處理,看我就夠了~

iOS觸摸事件全家桶

作者:林大鵬天地

鏈接:http://www.reibang.com/p/df86508e2811

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲤竹,一起剝皮案震驚了整個(gè)濱河市浪读,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辛藻,老刑警劉巖碘橘,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吱肌,居然都是意外死亡痘拆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門岩榆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來错负,“玉大人,你說我怎么就攤上這事勇边∮倘觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵粒褒,是天一觀的道長识颊。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么祥款? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任清笨,我火速辦了婚禮,結(jié)果婚禮上刃跛,老公的妹妹穿的比我還像新娘抠艾。我一直安慰自己,他們只是感情好桨昙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布检号。 她就那樣靜靜地躺著,像睡著了一般蛙酪。 火紅的嫁衣襯著肌膚如雪齐苛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天桂塞,我揣著相機(jī)與錄音凹蜂,去河邊找鬼。 笑死阁危,一個(gè)胖子當(dāng)著我的面吹牛玛痊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播欲芹,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼卿啡,長吁一口氣:“原來是場噩夢啊……” “哼吟吝!你這毒婦竟也來了菱父?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤剑逃,失蹤者是張志新(化名)和其女友劉穎浙宜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛹磺,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粟瞬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萤捆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裙品。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖俗或,靈堂內(nèi)的尸體忽然破棺而出市怎,到底是詐尸還是另有隱情,我是刑警寧澤辛慰,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布区匠,位于F島的核電站,受9級特大地震影響帅腌,放射性物質(zhì)發(fā)生泄漏驰弄。R本人自食惡果不足惜麻汰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望戚篙。 院中可真熱鬧五鲫,春花似錦、人聲如沸岔擂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智亮。三九已至忆某,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阔蛉,已是汗流浹背弃舒。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留状原,地道東北人聋呢。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像颠区,于是被迫代替她去往敵國和親削锰。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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