iOS開發(fā)之事件傳遞響應(yīng)鏈
當我們在使用微信等工具,點擊掃一掃,就能打開二維碼掃描視圖。在我們點擊屏幕的時候,iphone OS獲取到了用戶進行了“單擊”這一行為,操作系統(tǒng)把包含這些點擊事件的信息包裝成UITouch和UIEvent形式的實例,然后找到當前運行的程序,逐級尋找能夠響應(yīng)這個事件的對象,直到?jīng)]有響應(yīng)者響應(yīng)。這一尋找的過程,被稱作事件的響應(yīng)鏈,如下圖所示,不用的響應(yīng)者以鏈式的方式尋找
事件響應(yīng)鏈
一、響應(yīng)者
在iOS中,能夠響應(yīng)事件的對象都是UIResponder的子類對象。UIResponder提供了四個用戶點擊的回調(diào)方法,分別對應(yīng)用戶點擊開始、移動、點擊結(jié)束以及取消點擊,其中只有在程序強制退出或者來電時,取消點擊事件才會調(diào)用。
UIResponder的點擊事件
在自定義UIView為基類的控件時,我們可以重寫這幾個方法來進行點擊回調(diào)。在回調(diào)中,我們可以看到方法接收兩個參數(shù),一個UITouch對象的集合,還有一個UIEvent對象。這兩個參數(shù)分別代表的是點擊對象和事件對象。
1、事件對象
iOS使用UIEvent表示用戶交互的事件對象,在UIEvent.h文件中,我們可以看到有一個UIEventType類型的屬性,這個屬性表示了當前的響應(yīng)事件類型。分別有多點觸控、搖一搖以及遠程操作(在iOS之后新增了3DTouch事件類型)。在一個用戶點擊事件處理過程中,UIEvent對象是唯一的
2、點擊對象
UITouch表示單個點擊,其類文件中存在枚舉類型UITouchPhase的屬性,用來表示當前點擊的狀態(tài)。這些狀態(tài)包括點擊開始、移動、停止不動、結(jié)束和取消五個狀態(tài)。每次點擊發(fā)生的時候,點擊對象都放在一個集合中傳入UIResponder的回調(diào)方法中,我們通過集合中對象獲取用戶點擊的位置。其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當前點擊坐標點,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個點擊位置的坐標點。
為了確認UIView確實是通過UIResponder的點擊方法響應(yīng)點擊事件的,我創(chuàng)建了UIView的類別,并重寫+ (void)load方法,使用method_swizzling的方式交換點擊事件的實現(xiàn)
+ (void)load Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:)); Method custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesBegan:withEvent:)); method_exchangeImplementations(origin, custom); origin = class_getInstanceMethod([UIView class], @selector(touchesMoved:withEvent:)); custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesMoved:withEvent:)); method_exchangeImplementations(origin, custom); origin = class_getInstanceMethod([UIView class], @selector(touchesEnded:withEvent:)); custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesEnded:withEvent:)); method_exchangeImplementations(origin, custom); } - (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event { NSLog(@"%@ --- begin", self.class); [self lxd_touchesBegan: touches withEvent: event]; } - (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event { NSLog(@"%@ --- move", self.class); [self lxd_touchesMoved: touches withEvent: event]; } - (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event { NSLog(@"%@ --- end", self.class); [self lxd_touchesEnded: touches withEvent: event]; }
在新建的項目中,我分別創(chuàng)建了AView、BView、CView和DView四個UIView的子類,然后點擊任意一個位置:
項目結(jié)構(gòu)圖
在我點擊上圖綠色視圖的時候,控制臺輸出了下面的日志(日期部分已經(jīng)去除):
CView --- begin CView --- end
由此可見在我們點擊UIView的時候,是通過touches相關(guān)的點擊事件進行回調(diào)處理的。
除了touches回調(diào)的幾個點擊事件,手勢UIGestureRecognizer對象也可以附加在view上,來實現(xiàn)其他豐富的手勢事件。在view添加單擊手勢之后,原來的touchesEnded方法就無效了。最開始我一直認為view添加手勢之后,原有的touches系列方法全部無效。但是在測試demo中,發(fā)現(xiàn)view添加手勢之后,touchesBegan方法是有進行回調(diào)的,但是moved跟ended就沒有進行回調(diào)。因此,在系統(tǒng)的touches事件處理中,在touchesBegan之后,應(yīng)該是存在著一個調(diào)度后續(xù)事件(nextHandler)處理的方法,個人猜測事件調(diào)度的處理大致如下圖示:
事件調(diào)度
二、響應(yīng)鏈傳遞
上面已經(jīng)介紹了某個控件在接收到點擊事件時的處理,那么系統(tǒng)是怎么通過用戶點擊的位置找到處理點擊事件的view的呢?
在上文我們已經(jīng)說過了系統(tǒng)通過不斷查找下一個響應(yīng)者來響應(yīng)點擊事件,而所有的可交互控件都是UIResponder直接或者間接的子類,那么我們是否可以在這個類的頭文件中找到關(guān)鍵的屬性呢?
正好存在著這么一個方法:- (nullable UIResponder *)nextResponder,通過方法名我們不難發(fā)現(xiàn)這是獲取當前view的下一個響應(yīng)者,那么我們重寫touchesBegan方法,逐級獲取下一響應(yīng)者,直到?jīng)]有下一個響應(yīng)者位置。相關(guān)代碼如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UIResponder * next = [self nextResponder]; NSMutableString * prefix = @"".mutableCopy; while (next != nil) { NSLog(@"%@%@", prefix, [next class]); [prefix appendString: @"--"]; next = [next nextResponder]; } }
控制臺輸出的所有下級事件響應(yīng)者如下:
AView --UIView ----ViewController ------UIWindow --------UIApplication ----------AppDelegate
雖然結(jié)果非常有層次,但是從系統(tǒng)逐級查找響應(yīng)者的角度上來說,這個輸出的順序是剛好相反的。為什么會出現(xiàn)這種問題呢?我們可以看到輸出中存在一個ViewController類,說明UIViewController也是UIResponder的子類。但是我們可以發(fā)現(xiàn),controller是一個view的管理者,即便它是響應(yīng)鏈的成員之一,但是按照邏輯來說,控制器不應(yīng)該是系統(tǒng)查找對象之一,通過nextResponder方法查找的這個思路是不正確的。
后來,發(fā)現(xiàn)在UIView的頭文件中存在這么兩個方法,分別返回UIView和BOOL類型的方法:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
根據(jù)方法名,一個是根據(jù)點擊坐標返回事件是否發(fā)生在本視圖以內(nèi),另一個方法是返回響應(yīng)點擊事件的對象。通過這兩個方法,我們可以猜到,系統(tǒng)在收到點擊事件的時候通過不斷遍歷當前視圖上的子視圖的這些方法,獲取下一個響應(yīng)的視圖。因此,繼續(xù)通過method_swizzling方式修改這兩個方法的實現(xiàn),并且測試輸出如下:
UIStatusBarWindow can answer 1 UIStatusBar can answer 0 UIStatusBarForegroundView can answer 0 UIStatusBarServiceItemView can answer 0 UIStatusBarDataNetworkItemView can answer 0 UIStatusBarBatteryItemView can answer 0 UIStatusBarTimeItemView can answer 0 hit view: UIStatusBar hit view: UIStatusBarWindow UIWindow can answer 1 UIView can answer 1 hit view: _UILayoutGuide hit view: _UILayoutGuide AView can answer 1 DView can answer 0 hit view: DView BView can answer 0 hit view: BView hit view: AView hit view: UIView hit view: UIWindow ...... //下面是touches方法的輸出
最上面的UIStatusBar開頭的類型大家可能沒見過,但是不妨礙我們猜到這是狀態(tài)欄相關(guān)的一些視圖,具體可以查找蘋果的文檔中心(Xcode中快捷鍵shift+command+0打開)。從輸出中不難看出系統(tǒng)先調(diào)用pointInSide: WithEvent:判斷當前視圖以及這些視圖的子視圖是否能接收這次點擊事件,然后在調(diào)用hitTest: withEvent:依次獲取處理這個事件的所有視圖對象,在獲取所有的可處理事件對象后,開始調(diào)用這些對象的touches回調(diào)方法
通過輸出的方法調(diào)用,我們可以看到響應(yīng)查找的順序是: UIStatusBar相關(guān)的視圖 -> UIWindow -> UIView -> AView -> DView -> BView(系統(tǒng)在事件鏈傳遞的過程中一定會遍歷所有的子視圖判斷是否能夠響應(yīng)點擊事件),以本文demo為例,我們可以得出事件響應(yīng)鏈查找的圖示如下:
響應(yīng)者查找流程
那么在上面的查找響應(yīng)者流程完成之后,系統(tǒng)會將本次事件中的點擊轉(zhuǎn)換成UITouch對象,然后將這些對象和UIEvent類型的事件對象傳遞給touchesBegan方法,you
不僅如此,從上面輸出的nextResponder來看,所有的響應(yīng)者都是在查找中返回可響應(yīng)點擊的視圖。因此,我們可以推測出UIApplication對象維護著自己的一個響應(yīng)者棧,當pointInSide: withEvent:返回yes的時候,響應(yīng)者入棧。
響應(yīng)者棧
棧頂?shù)捻憫?yīng)者作為最優(yōu)先處理事件的對象,假設(shè)AView不處理事件,那么出棧,移交給UIView,以此下去,直到事件得到了處理或者到達AppDelegate后依舊未響應(yīng),事件被摒棄為止。通過這個機制我們也可以看到controller是響應(yīng)者棧中的例外,即便沒有pointInSide: withEvent:的方法返回可響應(yīng),controller依舊能夠入棧成為UIView的下一個響應(yīng)者。
、響應(yīng)鏈應(yīng)用
既然已經(jīng)知道了系統(tǒng)是怎么獲取響應(yīng)視圖的流程了,那么我們可以通過重寫查找事件處理者的方法來實現(xiàn)不規(guī)則形狀點擊。最常見的不規(guī)則視圖就是圓形視圖,在demo中我設(shè)置view的寬高為200,那么重寫方法事件如下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { const CGFloat halfWidth = 100; CGFloat xOffset = point.x - 100; CGFloat yOffset = point.y - 100; CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset); return radius <= halfWidth; }
最終的效果圖如下:
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助。
- 詳解iOS的UI開發(fā)中控制器的創(chuàng)建方法
- iOS開發(fā)中導航控制器的基本使用教程
- 深入講解iOS開發(fā)中應(yīng)用數(shù)據(jù)的存儲方式
- 詳解iOS開發(fā)中UItableview控件的數(shù)據(jù)刷新功能的實現(xiàn)
- 詳解iOS開發(fā)中使用storyboard創(chuàng)建導航控制器的方法
- iOS開發(fā)使用JSON解析網(wǎng)絡(luò)數(shù)據(jù)
- iOS開發(fā)使用XML解析網(wǎng)絡(luò)數(shù)據(jù)
- iOS應(yīng)用開發(fā)中UITabBarController標簽欄控制器使用進階
- iOS通過多種方式創(chuàng)建控制器
- iOS實現(xiàn)兩個控制器之間數(shù)據(jù)的雙向傳遞
相關(guān)文章
iOS UIScrollView滾動視圖/無限循環(huán)滾動/自動滾動的實例代碼
這篇文章主要介紹了iOS UIScrollView滾動視圖/無限循環(huán)滾動/自動滾動,需要的朋友可以參考下2017-02-02Objective-C中編程中一些推薦的書寫規(guī)范小結(jié)
這篇文章主要介紹了Objective-C的一些編程書寫規(guī)范小結(jié),包括類與方法等面向?qū)ο缶幊滔嚓P(guān)的代碼編寫風格,需要的朋友可以參考下2016-04-04iOS開發(fā)中Subview的事件響應(yīng)以及獲取subview的方法
這篇文章主要介紹了iOS開發(fā)中Subview的事件響應(yīng)以及獲取subview的方法,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-09-09IOS中MMDrawerController第三方抽屜效果的基本使用示例
這篇文章主要介紹了IOS中MMDrawerController第三方抽屜效果的基本使用示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02iOS中UITableView Cell實現(xiàn)自定義單選功能
本篇文章主要介紹了iOS中UITableView Cell實現(xiàn)自定義單選功能,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02