iOS開(kāi)發(fā)-事件傳遞響應(yīng)鏈

序言

當(dāng)我們?cè)谑褂梦⑿诺裙ぞ吣拢c(diǎn)擊掃一掃弯汰,就能打開(kāi)二維碼掃描視圖祷愉。在我們點(diǎn)擊屏幕的時(shí)候窗宦,iphone OS獲取到了用戶(hù)進(jìn)行了“單擊”這一行為赦颇,操作系統(tǒng)把包含這些點(diǎn)擊事件的信息包裝成UITouch和UIEvent形式的實(shí)例,然后找到當(dāng)前運(yùn)行的程序赴涵,逐級(jí)尋找能夠響應(yīng)這個(gè)事件的對(duì)象媒怯,直到?jīng)]有響應(yīng)者響應(yīng)。這一尋找的過(guò)程髓窜,被稱(chēng)作事件的響應(yīng)鏈扇苞,如下圖所示,不用的響應(yīng)者以鏈?zhǔn)降姆绞綄ふ?/p>

事件響應(yīng)鏈

響應(yīng)者

在iOS中寄纵,能夠響應(yīng)事件的對(duì)象都是UIResponder的子類(lèi)對(duì)象鳖敷。UIResponder提供了四個(gè)用戶(hù)點(diǎn)擊的回調(diào)方法,分別對(duì)應(yīng)用戶(hù)點(diǎn)擊開(kāi)始程拭、移動(dòng)定踱、點(diǎn)擊結(jié)束以及取消點(diǎn)擊,其中只有在程序強(qiáng)制退出或者來(lái)電時(shí)恃鞋,取消點(diǎn)擊事件才會(huì)調(diào)用崖媚。

UIResponder的點(diǎn)擊事件

在自定義UIView為基類(lèi)的控件時(shí),我們可以重寫(xiě)這幾個(gè)方法來(lái)進(jìn)行點(diǎn)擊回調(diào)恤浪。在回調(diào)中畅哑,我們可以看到方法接收兩個(gè)參數(shù),一個(gè)UITouch對(duì)象的集合水由,還有一個(gè)UIEvent對(duì)象荠呐。這兩個(gè)參數(shù)分別代表的是點(diǎn)擊對(duì)象和事件對(duì)象。

事件對(duì)象

iOS使用UIEvent表示用戶(hù)交互的事件對(duì)象砂客,在UIEvent.h文件中直秆,我們可以看到有一個(gè)UIEventType類(lèi)型的屬性,這個(gè)屬性表示了當(dāng)前的響應(yīng)事件類(lèi)型鞭盟。分別有多點(diǎn)觸控圾结、搖一搖以及遠(yuǎn)程操作(在iOS之后新增了3DTouch事件類(lèi)型)。在一個(gè)用戶(hù)點(diǎn)擊事件處理過(guò)程中齿诉,UIEvent對(duì)象是唯一的

點(diǎn)擊對(duì)象

UITouch表示單個(gè)點(diǎn)擊筝野,其類(lèi)文件中存在枚舉類(lèi)型UITouchPhase的屬性,用來(lái)表示當(dāng)前點(diǎn)擊的狀態(tài)粤剧。這些狀態(tài)包括點(diǎn)擊開(kāi)始歇竟、移動(dòng)、停止不動(dòng)抵恋、結(jié)束和取消五個(gè)狀態(tài)焕议。每次點(diǎn)擊發(fā)生的時(shí)候,點(diǎn)擊對(duì)象都放在一個(gè)集合中傳入U(xiǎn)IResponder的回調(diào)方法中弧关,我們通過(guò)集合中對(duì)象獲取用戶(hù)點(diǎn)擊的位置盅安。其中通過(guò)- (CGPoint)locationInView:(nullable UIView *)view獲取當(dāng)前點(diǎn)擊坐標(biāo)點(diǎn)唤锉,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個(gè)點(diǎn)擊位置的坐標(biāo)點(diǎn)。

為了確認(rèn)UIView確實(shí)是通過(guò)UIResponder的點(diǎn)擊方法響應(yīng)點(diǎn)擊事件的别瞭,我創(chuàng)建了UIView的類(lèi)別窿祥,并重寫(xiě)+ (void)load方法,使用method_swizzling的方式交換點(diǎn)擊事件的實(shí)現(xiàn)

+ (void)load? ? Method origin = class_getInstanceMethod([UIViewclass],@selector(touchesBegan:withEvent:));? ? Method custom = class_getInstanceMethod([UIViewclass],@selector(lxd_touchesBegan:withEvent:));? ? method_exchangeImplementations(origin, custom);? ? origin = class_getInstanceMethod([UIViewclass],@selector(touchesMoved:withEvent:));? ? custom = class_getInstanceMethod([UIViewclass],@selector(lxd_touchesMoved:withEvent:));? ? method_exchangeImplementations(origin, custom);? ? origin = class_getInstanceMethod([UIViewclass],@selector(touchesEnded:withEvent:));? ? custom = class_getInstanceMethod([UIViewclass],@selector(lxd_touchesEnded:withEvent:));? ? method_exchangeImplementations(origin, custom);}- (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent*)event{NSLog(@"%@ --- begin",self.class);? ? [selflxd_touchesBegan: touches withEvent: event];}- (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent*)event{NSLog(@"%@ --- move",self.class);? ? [selflxd_touchesMoved: touches withEvent: event];}- (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent*)event{NSLog(@"%@ --- end",self.class);? ? [selflxd_touchesEnded: touches withEvent: event];}

在新建的項(xiàng)目中蝙寨,我分別創(chuàng)建了AView晒衩、BView、CView和DView四個(gè)UIView的子類(lèi)墙歪,然后點(diǎn)擊任意一個(gè)位置:

項(xiàng)目結(jié)構(gòu)圖

在我點(diǎn)擊上圖綠色視圖的時(shí)候听系,控制臺(tái)輸出了下面的日志(日期部分已經(jīng)去除):

CView--- beginCView--- end

由此可見(jiàn)在我們點(diǎn)擊UIView的時(shí)候,是通過(guò)touches相關(guān)的點(diǎn)擊事件進(jìn)行回調(diào)處理的虹菲。

除了touches回調(diào)的幾個(gè)點(diǎn)擊事件跛锌,手勢(shì)UIGestureRecognizer對(duì)象也可以附加在view上,來(lái)實(shí)現(xiàn)其他豐富的手勢(shì)事件届惋。在view添加單擊手勢(shì)之后髓帽,原來(lái)的touchesEnded方法就無(wú)效了。最開(kāi)始我一直認(rèn)為view添加手勢(shì)之后脑豹,原有的touches系列方法全部無(wú)效郑藏。但是在測(cè)試demo中,發(fā)現(xiàn)view添加手勢(shì)之后瘩欺,touchesBegan方法是有進(jìn)行回調(diào)的必盖,但是moved跟ended就沒(méi)有進(jìn)行回調(diào)。因此俱饿,在系統(tǒng)的touches事件處理中暇咆,在touchesBegan之后熏瞄,應(yīng)該是存在著一個(gè)調(diào)度后續(xù)事件(nextHandler)處理的方法毙玻,個(gè)人猜測(cè)事件調(diào)度的處理大致如下圖示:

事件調(diào)度

響應(yīng)鏈傳遞

上面已經(jīng)介紹了某個(gè)控件在接收到點(diǎn)擊事件時(shí)的處理勺届,那么系統(tǒng)是怎么通過(guò)用戶(hù)點(diǎn)擊的位置找到處理點(diǎn)擊事件的view的呢?

在上文我們已經(jīng)說(shuō)過(guò)了系統(tǒng)通過(guò)不斷查找下一個(gè)響應(yīng)者來(lái)響應(yīng)點(diǎn)擊事件枣购,而所有的可交互控件都是UIResponder直接或者間接的子類(lèi)嬉探,那么我們是否可以在這個(gè)類(lèi)的頭文件中找到關(guān)鍵的屬性呢?

正好存在著這么一個(gè)方法:- (nullable UIResponder *)nextResponder棉圈,通過(guò)方法名我們不難發(fā)現(xiàn)這是獲取當(dāng)前view的下一個(gè)響應(yīng)者涩堤,那么我們重寫(xiě)touchesBegan方法,逐級(jí)獲取下一響應(yīng)者分瘾,直到?jīng)]有下一個(gè)響應(yīng)者位置胎围。相關(guān)代碼如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{UIResponder* next = [selfnextResponder];NSMutableString* prefix =@"".mutableCopy;while(next !=nil) {NSLog(@"%@%@", prefix, [nextclass]);? ? ? ? [prefix appendString:@"--"];? ? ? ? next = [next nextResponder];? ? }? ? }

控制臺(tái)輸出的所有下級(jí)事件響應(yīng)者如下:

AView--UIView----ViewController------UIWindow--------UIApplication----------AppDelegate

雖然結(jié)果非常有層次,但是從系統(tǒng)逐級(jí)查找響應(yīng)者的角度上來(lái)說(shuō),這個(gè)輸出的順序是剛好相反的白魂。為什么會(huì)出現(xiàn)這種問(wèn)題呢汽纤?我們可以看到輸出中存在一個(gè)ViewController類(lèi),說(shuō)明UIViewController也是UIResponder的子類(lèi)碧聪。但是我們可以發(fā)現(xiàn)冒版,controller是一個(gè)view的管理者液茎,即便它是響應(yīng)鏈的成員之一逞姿,但是按照邏輯來(lái)說(shuō),控制器不應(yīng)該是系統(tǒng)查找對(duì)象之一捆等,通過(guò)nextResponder方法查找的這個(gè)思路是不正確的滞造。

后來(lái),發(fā)現(xiàn)在UIView的頭文件中存在這么兩個(gè)方法栋烤,分別返回UIView和BOOL類(lèi)型的方法:

- (nullableUIView*)hitTest:(CGPoint)point withEvent:(nullableUIEvent*)event;// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent*)event;// default returns YES if point is in bounds

根據(jù)方法名谒养,一個(gè)是根據(jù)點(diǎn)擊坐標(biāo)返回事件是否發(fā)生在本視圖以?xún)?nèi),另一個(gè)方法是返回響應(yīng)點(diǎn)擊事件的對(duì)象明郭。通過(guò)這兩個(gè)方法买窟,我們可以猜到,系統(tǒng)在收到點(diǎn)擊事件的時(shí)候通過(guò)不斷遍歷當(dāng)前視圖上的子視圖的這些方法薯定,獲取下一個(gè)響應(yīng)的視圖始绍。因此,繼續(xù)通過(guò)method_swizzling方式修改這兩個(gè)方法的實(shí)現(xiàn)话侄,并且測(cè)試輸出如下:

UIStatusBarWindow can answer1UIStatusBar can answer0UIStatusBarForegroundView can answer0UIStatusBarServiceItemView can answer0UIStatusBarDataNetworkItemView can answer0UIStatusBarBatteryItemView can answer0UIStatusBarTimeItemView can answer0hitview:UIStatusBarhitview:UIStatusBarWindowUIWindow can answer1UIView can answer1hitview:_UILayoutGuidehitview:_UILayoutGuideAView can answer1DView can answer0hitview:DViewBView can answer0hitview:BViewhitview:AViewhitview:UIViewhitview:UIWindow......? //下面是touches方法的輸出

最上面的UIStatusBar開(kāi)頭的類(lèi)型大家可能沒(méi)見(jiàn)過(guò)亏推,但是不妨礙我們猜到這是狀態(tài)欄相關(guān)的一些視圖,具體可以查找蘋(píng)果的文檔中心(Xcode中快捷鍵shift+command+0打開(kāi))年堆。從輸出中不難看出系統(tǒng)先調(diào)用pointInSide: WithEvent:判斷當(dāng)前視圖以及這些視圖的子視圖是否能接收這次點(diǎn)擊事件吞杭,然后在調(diào)用hitTest: withEvent:依次獲取處理這個(gè)事件的所有視圖對(duì)象,在獲取所有的可處理事件對(duì)象后变丧,開(kāi)始調(diào)用這些對(duì)象的touches回調(diào)方法

通過(guò)輸出的方法調(diào)用芽狗,我們可以看到響應(yīng)查找的順序是:UIStatusBar相關(guān)的視圖 ->UIWindow->UIView->AView->DView->BView(系統(tǒng)在事件鏈傳遞的過(guò)程中一定會(huì)遍歷所有的子視圖判斷是否能夠響應(yīng)點(diǎn)擊事件),以本文demo為例痒蓬,我們可以得出事件響應(yīng)鏈查找的圖示如下:

響應(yīng)者查找流程

那么在上面的查找響應(yīng)者流程完成之后译蒂,系統(tǒng)會(huì)將本次事件中的點(diǎn)擊轉(zhuǎn)換成UITouch對(duì)象,然后將這些對(duì)象和UIEvent類(lèi)型的事件對(duì)象傳遞給touchesBegan方法谊却,you

不僅如此柔昼,從上面輸出的nextResponder來(lái)看,所有的響應(yīng)者都是在查找中返回可響應(yīng)點(diǎn)擊的視圖炎辨。因此捕透,我們可以推測(cè)出UIApplication對(duì)象維護(hù)著自己的一個(gè)響應(yīng)者棧,當(dāng)pointInSide: withEvent:返回yes的時(shí)候,響應(yīng)者入棧乙嘀。

響應(yīng)者棧

棧頂?shù)捻憫?yīng)者作為最優(yōu)先處理事件的對(duì)象末购,假設(shè)AView不處理事件,那么出棧虎谢,移交給UIView盟榴,以此下去,直到事件得到了處理或者到達(dá)AppDelegate后依舊未響應(yīng)婴噩,事件被摒棄為止擎场。通過(guò)這個(gè)機(jī)制我們也可以看到controller是響應(yīng)者棧中的例外,即便沒(méi)有pointInSide: withEvent:的方法返回可響應(yīng)几莽,controller依舊能夠入棧成為UIView的下一個(gè)響應(yīng)者迅办。

響應(yīng)鏈應(yīng)用

既然已經(jīng)知道了系統(tǒng)是怎么獲取響應(yīng)視圖的流程了,那么我們可以通過(guò)重寫(xiě)查找事件處理者的方法來(lái)實(shí)現(xiàn)不規(guī)則形狀點(diǎn)擊章蚣。最常見(jiàn)的不規(guī)則視圖就是圓形視圖站欺,在demo中我設(shè)置view的寬高為200,那么重寫(xiě)方法事件如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event{constCGFloathalfWidth =100;CGFloatxOffset = point.x -100;CGFloatyOffset = point.y -100;CGFloatradius = sqrt(xOffset * xOffset + yOffset * yOffset);returnradius <= halfWidth;}

最終的效果圖如下:

不規(guī)則形狀點(diǎn)擊

文集:iOS開(kāi)發(fā)

demo地址

轉(zhuǎn)載注明鏈接:http://sindrilin.com/ios-dev/2015/12/27/事件傳遞響應(yīng)鏈.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纤垂,一起剝皮案震驚了整個(gè)濱河市矾策,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌峭沦,老刑警劉巖贾虽,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異熙侍,居然都是意外死亡榄鉴,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)蛉抓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)庆尘,“玉大人,你說(shuō)我怎么就攤上這事巷送∈患桑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵笑跛,是天一觀的道長(zhǎng)付魔。 經(jīng)常有香客問(wèn)我,道長(zhǎng)飞蹂,這世上最難降的妖魔是什么几苍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮陈哑,結(jié)果婚禮上妻坝,老公的妹妹穿的比我還像新娘伸眶。我一直安慰自己,他們只是感情好刽宪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布厘贼。 她就那樣靜靜地躺著,像睡著了一般圣拄。 火紅的嫁衣襯著肌膚如雪嘴秸。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天庇谆,我揣著相機(jī)與錄音岳掐,去河邊找鬼。 笑死族铆,一個(gè)胖子當(dāng)著我的面吹牛岩四,可吹牛的內(nèi)容都是我干的哭尝。 我是一名探鬼主播哥攘,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼材鹦!你這毒婦竟也來(lái)了逝淹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤桶唐,失蹤者是張志新(化名)和其女友劉穎栅葡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體尤泽,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡欣簇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坯约。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片熊咽。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖闹丐,靈堂內(nèi)的尸體忽然破棺而出横殴,到底是詐尸還是另有隱情,我是刑警寧澤卿拴,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布衫仑,位于F島的核電站,受9級(jí)特大地震影響堕花,放射性物質(zhì)發(fā)生泄漏文狱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一缘挽、第九天 我趴在偏房一處隱蔽的房頂上張望瞄崇。 院中可真熱鬧陷虎,春花似錦、人聲如沸杠袱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)楣富。三九已至凿掂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纹蝴,已是汗流浹背庄萎。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留塘安,地道東北人糠涛。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像兼犯,于是被迫代替她去往敵國(guó)和親忍捡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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