iOS 響應鏈

當我們在使用微信等工具,點擊掃一掃,就能打開二維碼掃描視圖。在我們點擊屏幕的時候,iphone OS獲取到了用戶進行了“單擊”這一行為,操作系統把包含這些點擊事件的信息包裝成UITouch和UIEvent形式的實例,然后找到當前運行的程序,逐級尋找能夠響應這個事件的對象,直到沒有響應者響應。這一尋找的過程,被稱作事件的響應鏈,如下圖所示,不用的響應者以鏈式的方式尋找

事件響應鏈

響應者

在iOS中,能夠響應事件的對象都是UIResponder的子類對象。UIResponder提供了四個用戶點擊的回調方法,分別對應用戶點擊開始、移動、點擊結束以及取消點擊,其中只有在程序強制退出或者來電時,取消點擊事件才會調用。

UIResponder的點擊事件

在自定義UIView為基類的控件時,我們可以重寫這幾個方法來進行點擊回調。在回調中,我們可以看到方法接收兩個參數,一個UITouch對象的集合,還有一個UIEvent對象。這兩個參數分別代表的是點擊對象和事件對象。

事件對象

iOS使用UIEvent表示用戶交互的事件對象,在UIEvent.h文件中,我們可以看到有一個UIEventType類型的屬性,這個屬性表示了當前的響應事件類型。分別有多點觸控、搖一搖以及遠程操作(在iOS之后新增了3DTouch事件類型)。在一個用戶點擊事件處理過程中,UIEvent對象是唯一的

點擊對象

UITouch表示單個點擊,其類文件中存在枚舉類型UITouchPhase的屬性,用來表示當前點擊的狀態。這些狀態包括點擊開始、移動、停止不動、結束和取消五個狀態。每次點擊發生的時候,點擊對象都放在一個集合中傳入UIResponder的回調方法中,我們通過集合中對象獲取用戶點擊的位置。其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當前點擊坐標點,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個點擊位置的坐標點。

為了確認UIView確實是通過UIResponder的點擊方法響應點擊事件的,我創建了UIView的類別,并重寫+ (void)load方法,使用method_swizzling的方式交換點擊事件的實現

+?(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];

}

在新建的項目中,我分別創建了AView、BView、CView和DView四個UIView的子類,然后點擊任意一個位置:

項目結構圖

在我點擊上圖綠色視圖的時候,控制臺輸出了下面的日志(日期部分已經去除):

CView?---?begin

CView?---?end

由此可見在我們點擊UIView的時候,是通過touches相關的點擊事件進行回調處理的。

除了touches回調的幾個點擊事件,手勢UIGestureRecognizer對象也可以附加在view上,來實現其他豐富的手勢事件。在view添加單擊手勢之后,原來的touchesEnded方法就無效了。最開始我一直認為view添加手勢之后,原有的touches系列方法全部無效。但是在測試demo中,發現view添加手勢之后,touchesBegan方法是有進行回調的,但是moved跟ended就沒有進行回調。因此,在系統的touches事件處理中,在touchesBegan之后,應該是存在著一個調度后續事件(nextHandler)處理的方法,個人猜測事件調度的處理大致如下圖示:

事件調度

響應鏈傳遞

上面已經介紹了某個控件在接收到點擊事件時的處理,那么系統是怎么通過用戶點擊的位置找到處理點擊事件的view的呢?

在上文我們已經說過了系統通過不斷查找下一個響應者來響應點擊事件,而所有的可交互控件都是UIResponder直接或者間接的子類,那么我們是否可以在這個類的頭文件中找到關鍵的屬性呢?

正好存在著這么一個方法:- (nullable UIResponder *)nextResponder,通過方法名我們不難發現這是獲取當前view的下一個響應者,那么我們重寫touchesBegan方法,逐級獲取下一響應者,直到沒有下一個響應者位置。相關代碼如下:

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

}

}

控制臺輸出的所有下級事件響應者如下:

AView

--UIView

----ViewController

------UIWindow

--------UIApplication

----------AppDelegate

雖然結果非常有層次,但是從系統逐級查找響應者的角度上來說,這個輸出的順序是剛好相反的。為什么會出現這種問題呢?我們可以看到輸出中存在一個ViewController類,說明UIViewController也是UIResponder的子類。但是我們可以發現,controller是一個view的管理者,即便它是響應鏈的成員之一,但是按照邏輯來說,控制器不應該是系統查找對象之一,通過nextResponder方法查找的這個思路是不正確的。

后來,發現在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

根據方法名,一個是根據點擊坐標返回事件是否發生在本視圖以內,另一個方法是返回響應點擊事件的對象。通過這兩個方法,我們可以猜到,系統在收到點擊事件的時候通過不斷遍歷當前視圖上的子視圖的這些方法,獲取下一個響應的視圖。因此,繼續通過method_swizzling方式修改這兩個方法的實現,并且測試輸出如下:

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開頭的類型大家可能沒見過,但是不妨礙我們猜到這是狀態欄相關的一些視圖,具體可以查找蘋果的文檔中心(Xcode中快捷鍵shift+command+0打開)。從輸出中不難看出系統先調用pointInSide: WithEvent:判斷當前視圖以及這些視圖的子視圖是否能接收這次點擊事件,然后在調用hitTest: withEvent:依次獲取處理這個事件的所有視圖對象,在獲取所有的可處理事件對象后,開始調用這些對象的touches回調方法

通過輸出的方法調用,我們可以看到響應查找的順序是:UIStatusBar相關的視圖 ->UIWindow->UIView->AView->DView->BView(系統在事件鏈傳遞的過程中一定會遍歷所有的子視圖判斷是否能夠響應點擊事件),以本文demo為例,我們可以得出事件響應鏈查找的圖示如下:

響應者查找流程

那么在上面的查找響應者流程完成之后,系統會將本次事件中的點擊轉換成UITouch對象,然后將這些對象和UIEvent類型的事件對象傳遞給touchesBegan方法,you

不僅如此,從上面輸出的nextResponder來看,所有的響應者都是在查找中返回可響應點擊的視圖。因此,我們可以推測出UIApplication對象維護著自己的一個響應者棧,當pointInSide: withEvent:返回yes的時候,響應者入棧。

響應者棧

棧頂的響應者作為最優先處理事件的對象,假設AView不處理事件,那么出棧,移交給UIView,以此下去,直到事件得到了處理或者到達AppDelegate后依舊未響應,事件被摒棄為止。通過這個機制我們也可以看到controller是響應者棧中的例外,即便沒有pointInSide: withEvent:的方法返回可響應,controller依舊能夠入棧成為UIView的下一個響應者。

響應鏈應用

既然已經知道了系統是怎么獲取響應視圖的流程了,那么我們可以通過重寫查找事件處理者的方法來實現不規則形狀點擊。最常見的不規則視圖就是圓形視圖,在demo中我設置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;

}

最終的效果圖如下:

不規則形狀點擊

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 好奇觸摸事件是如何從屏幕轉移到APP內的?困惑于Cell怎么突然不能點擊了?糾結于如何實現這個奇葩響應需求?亦或是...
    Lotheve閱讀 58,006評論 51 603
  • 最近在寫一個圖片瀏覽的需求,一些地方我使用了響應者來處理,順便又去看看了官方文檔,這里記錄一下官方文檔,并給出一些...
    HelloAda閱讀 10,902評論 3 36
  • 在iOS開發中經常會涉及到觸摸事件。本想自己總結一下,但是遇到了這篇文章,感覺總結的已經很到位,特此轉載。作者:L...
    WQ_UESTC閱讀 6,129評論 4 26
  • 本文來自:http://ios.jobbole.com/84081/ 前言: 按照時間順序,事件的生命周期是這樣的...
    HackerOnce閱讀 2,859評論 1 10
  • 首先清楚兩個概念響應者:對用戶交互動作事件進行響應的對象。響應者鏈:成為處理事件的響應者的先后順序鏈。平時當我們點...
    mengyingguo閱讀 398評論 0 1