長治做網站公司網絡服務公司
響應者鏈
響應者鏈是由一系列鏈接在一起的響應者(UIResponser
之類:UIApplication
,UIViewController
,UIView
)注組成的。一般情況下,一條響應鏈開始于第一響應者,結束于application
對象。如果一個響應者不能處理事件,會將事件沿著響應者鏈傳到下一個響應者
響應者對象
在響應者鏈中,每個響應者對象都可以處理事件,也可以選擇將事件傳遞給下一個響應者對象進行處理,或者直接丟棄事件。響應者鏈中的每個響應者對象都可以重寫幾個方法來處理事件,這些方法包括touchesBegan:withEvent:
、touchesMoved:withEvent:
、touchesEnded:withEvent:
等等。
響應者對象都實現了UIResponder
協議,這里的實現UIResponder協議指UIApplication、UIViewController、UIView 都繼承于 UIResponder。
常見的響應者對象
- UIView:是iOS中最基本的用戶界面元素,可以接收用戶的觸摸事件并進行相關的處理。
- UIViewController:作為MVC模式中的控制器,可以響應用戶的觸摸事件,同時還可以管理一個或多個視圖控制器。
- UIWindow:是整個應用程序的窗口,它包含了一個或多個視圖,并且是接收和處理觸摸事件的最高層響應者對象。
- UIGestureRecognizer:是iOS中專門用來處理手勢事件的響應者對象,包括UITapGestureRecognizer、UIPanGestureRecognizer、UILongPressGestureRecognizer等等。
- UIScrollView:是一個可以滾動的視圖控件,它可以接收用戶的觸摸事件,并在觸摸拖動時進行滾動。
- UITableView:是iOS中常用的列表視圖控件,它可以顯示大量的數據,并且可以處理用戶的滑動、點擊等事件
響應者事件
iOS 中的事件可分為:觸摸事件(multitouch events)、加速計事件(accelerometer events):包括搖晃、傾斜、加速等設備運動、遠程控制事件(remote control events):可以來自于耳機、鎖屏界面和控制中心等,例如暫停音樂、切換歌曲等。
基本元素的了解
系統是怎么響應用戶的觸屏事件,這里有與用戶事件相關的類,它們分別是UITouch
, UIEvent
和UIResponder
UIResponder(事件響應者)
UIResponder是iOS中的一個基類,定義了一些接口,用于處理觸摸事件和鍵盤事件。所有能夠接受并處理事件的對象都繼承于UIResponder
- UIResponder內部提供了以下方法來處理事件
// 一根或者多根手指開始觸摸view,系統會自動調用view的下面方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指在view上移動,系統會自動調用view的下面方法(隨著手指的移動,會持續(xù)調用該方法)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指離開view,系統會自動調用view的下面方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 觸摸結束前,某個系統事件(例如電話呼入)會打斷觸摸過程,系統會自動調用view的下面方法[可選]
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
通過重寫UIresponder中定義的方法,開發(fā)者可以自己類中處理用戶事件,并做出相應的事件
UITouch(觸摸)
- UITouch是觸摸對象,一個手指一次觸摸屏幕就會生成一個UITouch對象
- 若兩個手指先后觸摸同一個位置,第一次觸摸時生成一個UITouch對象,第二次觸摸更新UITouch對象的tapCount屬性值由1變成2;如果兩個手指一前一后觸摸的位置不同,將會生成兩個UITouch對象,兩者沒有聯系。
- 每個UITouch對象會記錄觸摸的一些記錄,包括觸摸時間、位置、階段、所處的視圖和窗口等信息。 當手指移動時,系統會更新同一個UITouch對象,使之能夠保存該手指的觸摸位置;當手指離開屏幕上時,系統會銷毀響應的UITouch對象
- UITouch對象會在觸摸事件的過程中不斷更新,直到觸摸事件結束。在觸摸事件的過程中,系統會不斷向響應鏈的響應者發(fā)生事件,并將觸摸事件封裝成UIEvent對象進行傳遞。當觸摸事件結束時,系統就會銷毀相應的UITouch對象
UITouch的屬性
// 記錄了觸摸事件產生或變化時的時間,單位是秒 The relative time at which the acceleration event occurred(read-only)
@property(nonatomic,readonly) NSTimeInterval timestamp;
// 當前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase phase;
// touch down within a certain point within a certain amount of timen 短時間內點按屏幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSUInteger tapCount;
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);
// 觸摸產生時所處的窗口
@property(nullable,nonatomic,readonly, strong) UIWindow *window;
// 觸摸產生時所處的視圖
@property(nullable,nonatomic,readonly, strong) UIView *view;
// The gesture-recognizer objects currently attached to the view.
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers
UITouch方法
/*返回值表示觸摸在view上的位置
這里返回的位置是針對view的坐標系的(以view的左上角為原點(0, 0))
調用時傳入的view參數為nil的話,返回的是觸摸點在UIWindow的位置*/
- (CGPoint)locationInView:(nullable UIView *)view;
// 該方法記錄了前一個觸摸點的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;// Use these methods to gain additional precision that may be available from touches.
// Do not use precise locations for hit testing. A touch may hit test inside a view, yet have a precise location that lies just outside.//獲取指定視圖上的精確觸摸位置,該方法會考慮到多點觸控時不同觸點之間的偏移。
- (CGPoint)preciseLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
// 獲取指定視圖上上一次觸摸的精確位置。
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
UIEvent(事件)
UIEvent
是 iOS 中用于表示觸摸事件的類,一個 UIEvent 對象包含了所有與觸摸事件相關的信息,比如觸摸的位置、時間、階段,以及多點觸控時不同觸點之間的狀態(tài)等等。UIEvent 對象是由系統自動創(chuàng)建和管理的,通常情況下不需要手動創(chuàng)建。每產生一個事件,就會產生一個 UIEvent 對象,UIEvent 稱為事件對象。
事件類型屬性
//事件類型,枚舉值包括觸摸、運動、遙控等。
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);// 事件子類型,對于觸摸事件,其子類型包括touch down、touch move、touch up等。
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
產生時間的事件屬性
事件發(fā)生的時間戳,單位為秒。
@property(nonatomic,readonly) NSTimeInterval timestamp;
事件的傳遞和響應
- 步驟一:尋找目標,在iOS的視圖層次結構中找到事件的最終接受者
- 步驟二:事件響應·,基于iOS響應者鏈處理觸摸事件
事件的傳遞:尋找事件的第一響應者(Hit_Testing)
當一個事件發(fā)生時,事件會從父控件傳給子控件
也就是說由
- 硬件 -> 系統 ->
UIApplication
->UIWindow
->SuperView
->SubView
以上就是事件的傳遞,也就是尋找第一響應者的過程。
符合第一響應者的條件包括:
- touch事件的位置在響應者區(qū)域內 pointInside:withEvent: == YES
- 響應者 self.hidden != NO
- 響應者 self.alpha > 0.01
- 響應者 self.userInteractionEnabled = YES
- 遍歷 subview 時,是從上往下順序遍歷的,即 view.subviews 的 + + + lastObject 到 firstObject 的順序,找到合適的響應者view,即停止遍歷.
第一響應者對于接收到的事件的三種操作:
- 不攔截,默認操作。事件會自動沿著默認的響應者鏈往下傳遞
- 攔截,不再往下分發(fā)事件。重寫
touchesBegan:withEvent:
進行事件處理,不調用父類的touchesBegan:withEvent:
- 攔截,繼續(xù)往下分發(fā)事件。重寫
touchesBegan:withEvent:
進行事件處理,同時調用父類的touchesBegan:withEvent:
將事件往下傳遞
事件的響應:一旦事件的第一響應者確定了,這個事件的響應鏈就確定了
下圖是官網對于響應者鏈的實例展示
每個響應者對象(UIResponder)對象都有一個nextResponder
方法,用于獲取響應者鏈中當前對象的下一個響應者。
- 圖中虛線箭頭是指若該
UIView
是作為UIViewController
根視圖存在的,則其nextResponder
為UIViewController
對象; - 若是直接add在
UIWindow
上的,則其nextResponder
為UIWindow
對象。
若觸摸發(fā)生在UITextField
上,則事件的傳遞順序是:
UITextField
——>UIView
——>UIView
——>UIViewController
——>UIWindow
——>UIApplication
——>UIApplicationDelegation
雖然兩個傳遞過程都設計到父子控件的傳遞,但它們的傳遞順序和目的不同,觸摸事件的傳遞過程主要是為了找到最合適的空間來處理事件,而響應者鏈傳遞過程是為了讓控件的響應者對象能夠逐級處理事件。
事件的生命周期
手指觸摸屏幕的一刻,系統會生成一個觸摸事件。經過IPC進程間通信,事件最終被傳遞給了合適的應用。
(一)系統響應階段
- 屏幕感應到觸碰后,將事件交給IOKit處理,IOKit是監(jiān)測硬件的框架。IOKit將觸摸事件封裝成一個IOHIDEvent對象,并通過mach port傳遞給SpringBoard進程。
mach port是進程端口,各個進程之間通過它進行通信;
SpringBoard.app是一個系統進程,可以理解為桌面系統,可以統一管理和分發(fā)系統接收到的觸摸事件;
- SpringBoard.app進程收到觸摸事件,觸發(fā)主線程RunLoop的source1事件源的回調。SpringBoard.app會根據當前桌面的狀態(tài),判斷應該由誰響應此次觸摸事件。如果沒有APP在運行,則由SpringBoard處理該事件;如果有APP在運行,則由APP處理該事件;
(二)APP響應階段
- APP進程的mach port接收到SpringBoard進程傳遞來的觸摸事件,主線程的RunLoop被喚醒,觸發(fā)source1回調;
- source1回調觸發(fā)了一個source0回調,將接收到的IOHIDEvent對象封裝成UIEvent對象;
- source0回調內部將觸摸事件添加到UIApplication對象的事件隊列中。事件出隊列后,UIApplication開始尋找一個最佳響應者的過程,這個過程又稱為hit-testing,具體細節(jié)在第二個主題尋找最佳響應者中闡述;
- 找到最佳響應者后,事件就在響應鏈中傳遞和響應,這里涉及到“事件的響應和響應鏈中的傳遞”;
- 經過上述流程,觸摸事件要么被某個響應對象捕獲后釋放,要么沒有找到能響應的對象被釋放;
總結:觸摸事件從觸屏產生后,有IOKit
將觸摸事件傳遞給SpringBoard
進程,再由SpingBoard
分發(fā)給當前前臺APP
處理,觸發(fā)事件響應者鏈事件。
完整的觸摸過程
一個完整的觸摸事件流程通常包括以下幾個步驟:
- 手指觸摸到屏幕,系統會創(chuàng)建一個與手指相關聯的
UITouch
對象,并將其加入到系統中的事件隊列中。 - 系統會將該事件發(fā)送給當前
UIWindow
對象,即調用UIWindow
對象的touchesBegan(_:with:)
方法,并將該事件傳遞給子視圖。 - 從根視圖開始,系統會通過遞歸調用
hitTest(_:with:)
方法,尋找響應該事件的視圖。在每個視圖中,系統都會調用point(inside:with:)
方法,判斷該視圖是否包含該事件的觸摸點。 - 一旦找到了響應該事件的視圖,系統會將該事件發(fā)送給該視圖,即調用該視圖的
touchesBegan(:with:)
方法。 - 在該視圖的
touchesBegan(:with:)
方法中,開發(fā)者可以對該事件做出相應的處理,比如更改視圖的狀態(tài)、更新視圖的內容等。 - 如果該事件需要傳遞給其它視圖進行處理,開發(fā)者可以手動調用
next
方法,將該事件傳遞給下一個響應者。 - 當手指離開屏幕時,系統會將一個
touch
對象的phase
屬性設置為.ended
,并將該touch
對象從事件隊列中移除。 - 當前的
UIWindow
對象會將該事件發(fā)送給響應者鏈中的下一個響應者。如果沒有下一個響應者,則該事件的響應過程結束。
總結:
- 當觸摸事件發(fā)生后,系統會自動生成一個
UIEvent
對象,記錄事件發(fā)生的事件和類型 - 然后系統會把
UIEvent
事件加入到一個由UIApplocation
管理的事件隊列中 - 然后
UIApplication
會講事件分發(fā)給UIWindow
,主窗口會在視圖層次結構中找到一個合適的響應者對象來處理觸摸事件。 - 不斷遞歸調用
hitTest
方法來找到第一響應者 - 如果第一響應者無法響應事件,那么會按照響應者鏈往上傳遞,也就是傳遞給自己的父視圖
- 一直傳遞直到
UIApplication
,如果都無法響應,事件就會被丟棄