<簡書 — 劉小壯> https://www.jianshu.com/p/b0884faae603app
很久沒寫博客了,先後算起來恰好有一年了。這期間博客也不是一直沒變化,細心的同窗應該能發現,我一直在回覆評論區和私信的問題,還更新了好幾篇以前的博客。ide
去年是有意義的一年,從各個方面我也學到了很多的東西,也不侷限於技術方面。不少人都寫年終總結,我比較懶就不寫了,心裏作自我總結吧,哈哈。函數
迴歸正題,在項目中常常會遇到各類手勢或者點擊事件處理之類的,這些都屬於響應事件處理。可是不少人對iOS中的響應事件處理並不清楚,常常會遇到手勢衝突、事件不響應之類的問題,因此就去查博客。 可是如今不少博客寫的並非很完整,或者說質量並不高,我這兩天抽時間把我所學習和理解的iOS事件處理寫出來,供各位參考。oop
**UIResponder
是iOS中用於處理用戶事件的API,能夠處理觸摸事件、按壓事件(3D touch)
、遠程控制事件、硬件運動事件。**能夠經過touchesBegan
、pressesBegan
、motionBegan
、remoteControlReceivedWithEvent
等方法,獲取到對應的回調消息。UIResponder
不僅用來接收事件,還能夠處理和傳遞對應的事件,若是當前響應者不能處理,則轉發給其餘合適的響應者處理。學習
應用程序經過響應者來接收和處理事件,響應者能夠是繼承自UIResponder
的任何子類,例如UIView
、UIViewController
、UIApplication
等。當事件來到時,系統會將事件傳遞給合適的響應者,而且將其成爲第一響應者。測試
第一響應者未處理的事件,將會在響應者鏈中進行傳遞,傳遞規則由UIResponder
的nextResponder
決定,能夠經過重寫該屬性來決定傳遞規則。當一個事件到來時,第一響應者沒有接收消息,則順着響應者鏈向後傳遞。ui
查找第一響應者時,有兩個很是關鍵的API
,查找第一響應者就是經過不斷調用子視圖的這兩個API
完成的。代理
調用方法,獲取到被點擊的視圖,也就是第一響應者。code
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:
方法內部會經過調用這個方法,來判斷點擊區域是否在視圖上,是則返回YES
,不是則返回NO
。對象
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
應用程序接收到事件後,將事件交給keyWindow
並轉發給根視圖,根視圖按照視圖層級逐級遍歷子視圖,而且遍歷的過程當中不斷判斷視圖範圍,並最終找到第一響應者。
從keyWindow
開始,向前逐級遍歷子視圖,不斷調用UIView
的hitTest:withEvent:
方法,經過該方法查找在點擊區域中的視圖後,並繼續調用返回視圖的子視圖的hitTest:withEvent:
方法,以此類推。若是子視圖不在點擊區域或沒有子視圖,則當前視圖就是第一響應者。
在hitTest:withEvent:
方法中,會從上到下遍歷子視圖,並調用subViews
的pointInside:withEvent:
方法,來找到點擊區域內且最上面的子視圖。若是找到子視圖則調用其hitTest:withEvent:
方法,並繼續執行這個流程,以此類推。若是子視圖不在點擊區域內,則忽略這個視圖及其子視圖,繼續遍歷其餘視圖。
能夠經過重寫對應的方法,控制這個遍歷過程。經過重寫pointInside:withEvent:
方法,來作本身的判斷並返回YES
或NO
,返回點擊區域是否在視圖上。經過重寫hitTest:withEvent:
方法,返回被點擊的視圖。
此方法在遍歷視圖時,忽略如下三種狀況的視圖,若是視圖具備如下特徵則忽略。可是視圖的背景顏色是clearColor
,並不在忽略範圍內。
hidden
等於YES。alpha
小於等於0.01。userInteractionEnabled
爲NO。若是點擊事件是發生在視圖外,但在其子視圖內部,子視圖也不能接收事件併成爲第一響應者。這是由於在其父視圖進行hitTest:withEvent:
的過程當中,就會將其忽略掉。
UIApplication
接收到事件,將事件傳遞給keyWindow
。keyWindow
遍歷subViews
的hitTest:withEvent:
方法,找到點擊區域內合適的視圖來處理事件。UIView
的子視圖也會遍歷其subViews
的hitTest:withEvent:
方法,以此類推。UIApplication
。nextResponder
方法,一直找響應者鏈中能處理該事件的對象。UIApplication
後仍然沒有能處理該事件的對象,則該事件被廢棄。模擬代碼
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) { return nil; } BOOL inside = [self pointInside:point withEvent:event]; if (inside) { NSArray *subViews = self.subviews; // 對子視圖從上向下找 for (NSInteger i = subViews.count - 1; i >= 0; i--) { UIView *subView = subViews[i]; CGPoint insidePoint = [self convertPoint:point toView:subView]; UIView *hitView = [subView hitTest:insidePoint withEvent:event]; if (hitView) { return hitView; } } return self; } return nil; }
如上圖所示,響應者鏈以下:
UITextField
後其會成爲第一響應者。textField
未處理事件,則會將事件傳遞給下一級響應者鏈,也就是其父視圖。UIViewController
的View
。View
未處理事件,則會交給控制器處理。UIWindow
。UIApplication
。UIApplicationDelegate
,若是其未處理則丟棄事件。事件經過UITouch
進行傳遞,在事件到來時,第一響應者會分配對應的UITouch
,UITouch
會一直跟隨着第一響應者,而且根據當前事件的變化UITouch
也會變化,當事件結束後則UITouch
被釋放。
UIViewController
沒有hitTest:withEvent:
方法,因此控制器不參與查找響應視圖的過程。可是控制器在響應者鏈中,若是控制器的View
不處理事件,會交給控制器來處理。控制器不處理的話,再交給View
的下一級響應者處理。
hitTest:withEvent:
方法時,若是該視圖是hidden
等於NO的那三種被忽略的狀況,則改視圖返回nil
。UIImageView
的userInteractionEnabled
默認爲NO,若是想要UIImageView
響應交互事件,將屬性設置爲YES便可響應事件。有時候想讓指定視圖來響應事件,再也不向其子視圖繼續傳遞事件,能夠經過重寫hitTest:withEvent:
方法。在執行到方法後,直接將該視圖返回,而再也不繼續遍歷子視圖,這樣響應者鏈的終端就是當前視圖。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return self; }
在開發過程當中,常常會遇到子視圖顯示範圍超出父視圖的狀況,這時候能夠重寫該視圖的pointInside:withEvent:
方法,將點擊區域擴大到可以覆蓋全部子視圖。
假設有上面的視圖結構,SuperView
的Subview
超出了其視圖範圍,若是點擊Subview
在父視圖外面的部分,則不能響應事件。因此經過重寫pointInside:withEvent:
方法,將響應區域擴大爲虛線區域,包含SuperView
的全部子視圖,便可讓子視圖響應事件。
若是想讓響應者鏈中,每一級UIResponder
均可以響應事件,能夠在每級UIResponder
中都實現touches
並調用super
方法,便可實現響應者鏈事件逐級傳遞。
只不過這並不包含UIControl
子類以及UIGestureRecognizer
的子類,這兩類會直接打斷響應者鏈。
若是有事件到來時,視圖有附加的手勢識別器,則手勢識別器優先處理事件。若是手勢識別器沒有處理事件,則將事件交給視圖處理,視圖若是未處理則順着響應者鏈繼續向後傳遞。
當響應者鏈和手勢同時出現時,也就是既實現了touches
方法又添加了手勢,會發現touches
方法有時會失效,這是由於手勢的執行優先級是高於響應者鏈的。
事件到來後先會執行hitTest
和pointInside
操做,經過這兩個方法找到第一響應者,這個在上面已經詳細講過了。當找到第一響應者並將其返回給UIApplication
後,UIApplication
會向第一響應者派發事件,而且遍歷整個響應者鏈。若是響應者鏈中可以處理當前事件的手勢,則將事件交給手勢處理,並調用touches
的cancelled
方法將響應者鏈取消。
在UIApplication
向第一響應者派發事件,而且遍歷響應者鏈查找手勢時,會開始執行響應者鏈中的touches
系列方法。會先執行touchesBegan
和touchesMoved
方法,若是響應者鏈可以繼續響應事件,則執行touchesEnded
方法表示事件完成,若是將事件交給手勢處理則調用touchesCancelled
方法將響應者鏈打斷。
根據蘋果的官方文檔,手勢不參與響應者鏈傳遞事件,可是也經過hitTest
的方式查找響應的視圖,手勢和響應者鏈同樣都須要經過hitTest
方法來肯定響應者鏈的。在UIApplication
向響應者鏈派發消息時,只要響應者鏈中存在可以處理事件的手勢,則手勢響應事件,若是手勢不在響應者鏈中則不能處理事件。
Apple UIGestureRecognizer Documentation
根據上面的手勢和響應者鏈的處理規則,咱們會發現UIButton
或者UISlider
等控件,並不符合這個處理規則。UIButton
能夠在其父視圖已經添加tapGestureRecognizer
的狀況下,依然正常響應事件,而且tap
手勢不響應。
以UIButton
爲例,UIButton
也是經過hitTest
的方式查找第一響應者的。區別在於,若是UIButton
是第一響應者,則直接由UIApplication
派發事件,不經過Responder Chain
派發。若是其不能處理事件,則交給手勢處理或響應者鏈傳遞。
不僅UIButton
是直接由UIApplication
派發事件的,全部繼承自UIControl
的類,都是由UIApplication
直接派發事件的。
爲了有依據的推斷響應事件的實現和傳遞機制,咱們作如下測試。
假設RootView
、SuperView
、Button
都實現touches
方法,而且Button
添加buttonAction:
的action
,點擊button
後的調用以下。
RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Button -> hitTest:withEvent: Button -> pointInside:withEvent: RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: Button -> touchesBegan:withEvent: Button -> touchesEnded:withEvent: Button -> buttonAction:
仍是上面的視圖結構,咱們給RootView
加上UITapGestureRecognizer
手勢,而且經過tapAction:
方法接收回調,點擊上面的SuperView
後,方法調用以下。
RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Button -> hitTest:withEvent: Button -> pointInside:withEvent: RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: RootView -> gestureRecognizer:shouldReceivePress: RootView -> gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: SuperView -> touchesBegan:withEvent: RootView -> gestureRecognizerShouldBegin: RootView -> tapAction: SuperView -> touchesCancelled:
上面的視圖中Subview1
、Subview2
、Subview3
是同級視圖,都是SuperView
的子視圖。咱們給Subview1
加上UITapGestureRecognizer
手勢,而且經過subView1Action:
方法接收回調,點擊上面的Subview3
後,方法調用以下。
SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Subview3 -> hitTest:withEvent: Subview3 -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Subview3 -> touchesBegan:withEvent: Subview3 -> touchesEnded:withEvent:
經過上面的例子來看,雖然Subview1
在Subview3
的下面,而且添加了手勢,點擊區域是在Subview1
和Subview3
兩個視圖上的。可是因爲通過hitTest
和pointInside
以後,響應者鏈中並無Subview1
,因此Subview1
的手勢並無被響應。
根據咱們上面的測試,推斷iOS響應事件的優先級,以及總體的響應邏輯。
當事件到來時,會經過hitTest
和pointInside
兩個方法,從Window
開始向上面的視圖查找,找到第一響應者的視圖。找到第一響應者後,系統會判斷其是繼承自UIControl
仍是UIResponder
,若是是繼承自UIControl
,則直接經過UIApplication
直接向其派發消息,而且再也不向響應者鏈派發消息。
若是是繼承自UIResponder
的類,則調用第一響應者的touchesBegin
,而且不會當即執行touchesEnded
,而是調用以後順着響應者鏈向後查找。若是在查找過程當中,發現響應者鏈中有的視圖添加了手勢,則進入手勢的代理方法中,若是代理方法返回能夠響應這個事件,則將第一響應者的事件取消,並調用其touchesCanceled
方法,而後由手勢來響應事件。
若是手勢不能處理事件,則交給第一響應者來處理。若是第一響應者也不能響應事件,則順着響應者鏈繼續向後查找,直到找到可以處理事件的UIResponder
對象。若是找到UIApplication
尚未對象響應事件的話,則將此次事件丟棄。
在UIApplication
接收到響應事件以前,還有更復雜的系統級的處理,處理流程大體以下。
系統經過IOKit.framework
來處理硬件操做,其中屏幕處理也經過IOKit
完成(IOKit
多是註冊監聽了屏幕輸出的端口) 當用戶操做屏幕,IOKit
收到屏幕操做,會將此次操做封裝爲IOHIDEvent
對象。經過mach port
(IPC進程間通訊)將事件轉發給SpringBoard
來處理。
SpringBoard
是iOS系統的桌面程序。SpringBoard
收到mach port
發過來的事件,喚醒main runloop
來處理。 main runloop
將事件交給source1
處理,source1
會調用__IOHIDEventSystemClientQueueCallback()
函數。
函數內部會判斷,是否有程序在前臺顯示,若是有則經過mach port
將IOHIDEvent
事件轉發給這個程序。 若是前臺沒有程序在顯示,則代表SpringBoard
的桌面程序在前臺顯示,也就是用戶在桌面進行了操做。 __IOHIDEventSystemClientQueueCallback()
函數會將事件交給source0
處理,source0
會調用__UIApplicationHandleEventQueue()
函數,函數內部會作具體的處理操做。
例如用戶點擊了某個應用程序的icon,會將這個程序啓動。 應用程序接收到SpringBoard
傳來的消息,會喚醒main runloop
並將這個消息交給source1
處理,source1
調用__IOHIDEventSystemClientQueueCallback()
函數,在函數內部會將事件交給source0
處理,並調用source0
的__UIApplicationHandleEventQueue()
函數。 在__UIApplicationHandleEventQueue()
函數中,會將傳遞過來的IOHIDEvent
轉換爲UIEvent
對象。
在函數內部,調用UIApplication
的sendEvent:
方法,將UIEvent
傳遞給第一響應者或UIControl
對象處理,在UIEvent
內部包含若干個UITouch
對象。
source1
是runloop
用來處理mach port
傳來的系統事件的,source0
是用來處理用戶事件的。 source1
收到系統事件後,都會調用source0
的函數,因此最終這些事件都是由source0
處理的。
在開發中,有時會有找到當前View
對應的控制器的需求,這時候就能夠利用咱們上面所學,根據響應者鏈來找到最近的控制器。
在UIResponder
中提供了nextResponder
方法,經過這個方法能夠找到當前響應環節的上一級響應對象。能夠從當前UIView
開始不斷調用nextResponder
,查找上一級響應者鏈的對象,就能夠找到離本身最近的UIViewController
。
示例代碼:
- (UIViewController *)parentController { UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController *)responder; } responder = [responder nextResponder]; } return nil; }