序言
當(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)擊
轉(zhuǎn)載注明鏈接:http://sindrilin.com/ios-dev/2015/12/27/事件傳遞響應(yīng)鏈.html