本次筆記主要是整理一下關于 iOS 中關于事件傳遞和響應機制辕坝,參考了一些其他資料加上自己的理解。
事件 Events
定義是 objects sent to an app informing user actions.
iOS 中的事件
iOS 有三種事件類型:
- 觸摸事件:單點侄刽、多點觸控以及各種手勢操作色鸳;
- 加速計事件:重力漫谷、加速度傳感器等;
- 遠程控制事件:遠程遙控iOS設備多媒體播放等冷溶;
響應者對象 UIResponder
在iOS中不是任何對象都能處理事件,只有繼承了UIResponder的對象才能接受并處理事件尊浓,我們稱之為“響應者對象”逞频。以下都是繼承自UIResponder的,所以都能接收并處理事件眠砾。
- UIApplication
- UIViewController
- UIView
以上三個類都繼承自 UIResponder 虏劲,所以都可以接收并處理事件。
UIResponder 中提供了以下對象方法來處理接收到的事件褒颈。
//UIResponder內部提供了以下方法來處理事件觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
//加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
//遠程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
當用手指觸摸 UIView 的一個實例的時候柒巫,就會產生觸摸事件 UIEventTypeTouches,而接收對象 UIView 就是一個Responder object。 一個事件可以被多個 Responder 接收谷丸,第一個接收事件的對象就是 firstResponder堡掏。
觸摸事件
UIView 的觸摸事件處理方法有以下幾種:
// UIView是UIResponder的子類,可以覆蓋下列4個方法處理不同的觸摸事件
// 一根或者多根手指開始觸摸view刨疼,系統(tǒng)會自動調用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移動泉唁,系統(tǒng)會自動調用view的下面方法(隨著手指的移動,會持續(xù)調用該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指離開view揩慕,系統(tǒng)會自動調用view的下面方法- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 觸摸結束前亭畜,某個系統(tǒng)事件(例如電話呼入)會打斷觸摸過程,系統(tǒng)會自動調用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch對象
以上四個方法是由系統(tǒng)自動調用的迎卤,所以可以通過重寫該方法來處理一些事件拴鸵。需要注意的是重寫以上四個方法,如果是處理UIView的觸摸事件蜗搔,必須要自定義UIView子類繼承自UIView劲藐。沒有UIView的 .m 文件,只能通過子類繼承父類樟凄,重寫子類方法的方式處理UIView的觸摸事件聘芜。如果是處理UIViewController的觸摸事件,那么在控制器的.m文件中直接重寫即可缝龄。
UIView 的拖拽事件
實現(xiàn)拖拽是讓UIView隨著手指的移動而移動汰现,重寫touchsMoved:withEvent:
挂谍,需要用到UITouch
對象。
- 手指觸摸屏幕時服鹅,會創(chuàng)建一個與手指相關的UITouch對象
- 一根手指對應一個UITouch對象
- 如果兩根手指同時觸摸一個view凳兵,那么view只會調用一次touchesBegan:withEvent:方法,touches參數(shù)中裝著2個UITouch對象
- 手指一前一后分開觸摸同一個view企软,那么view會分別調用2次touchesBegan:withEvent:方法庐扫,并且每次調用時的touches參數(shù)中只包含一個UITouch對象
UITouch對象的作用:
- 保存著跟手指相關的信息,比如觸摸的位置仗哨、時間形庭、階段
- 手指移動時,系統(tǒng)會更新同一個UITouch對象厌漂,使之能夠一直保存該手指在的觸摸位置
- 手指離開屏幕時萨醒,系統(tǒng)會銷毀相應的UITouch對象
UITouch 對象的屬性
- 觸摸產生時所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
- 觸摸產生時所處的視圖
@property(nonatomic,readonly,retain) UIView *view
- 短時間內點按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSUInteger tapCount
- 記錄了觸摸事件產生或變化時的時間,單位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp
- 當前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase phase
UITouch 的方法
-
-(CGPoint)locationInView:(UIView*)view
- 返回值表示觸摸在view上的位置
- 這里返回的位置是針對view的坐標系的(以view的左上角為原點(0, 0))
- 調用時傳入的view參數(shù)為nil的話,返回的是觸摸點在UIWindow的位置
-
- (CGPoint)previousLocationInView:(UIView *)view
該方法記錄了前一個觸摸點的位置
iOS 中事件的產生和傳遞
事件傳遞
- 發(fā)生觸摸事件后,系統(tǒng)會將該事件 UIEvent 加入到一個由UIApplication管理的事件隊列中
- UIApplication會從事件隊列中取出最前面的事件,并將事件分發(fā)下去以便處理,通
常,先發(fā)送事件給應用程序的主窗口(keyWindow) - 主窗又會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個事件處理
過程的第一步 - 找到合適的視圖控件后,就會調用視圖控件的touches方法來作具體的事件處理
- touchesBegan...
- touchesMoved...
- touchedEnded...
舉例:
- 觸摸事件的傳遞是從父控件傳遞到子控件
- 點擊了綠色的view:
UIApplication -> UIWindow -> 白色 -> 綠色 - 點擊了藍色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色 - 點擊了黃色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色 -> 黃色 - 如果父控件不能接收觸摸事件,那么子控件就不可能接收到觸摸事件
- 如何找到最合適的控件來處理事件?
- 自己是否能接收觸摸事件?
- 觸摸點是否在自己身上?
- 從后往前遍歷子控件,重復前面的兩個步驟
- 如果沒有符合條件的子控件,那么就自己最適合處理
找到最合適的 view 后苇倡,就會調用該 View 的 touches 方法處理具體的事件富纸,只有找到最合適的 View,把事件傳遞給 View 后才會調用 touches 等方法來進行事件處理。
具體如何找到最合適的 View 來處理事件
整個過程類似遞歸調用旨椒。
舉例說明晓褪,假如點在黃色 View 上,觸摸事件是從父控件傳遞到子控件综慎,UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色 -> 黃色涣仿。點在黃色View上,同時也點在白色示惊、橙色和藍色上好港,系統(tǒng)如何判斷是點在黃色上呢?首先點到黃色后會產生一個觸摸事件米罚,系統(tǒng)會將該事件 UIEvent 加入到一個由 UIApplication 管理的事件隊列中钧汹,UIApplication會從事件隊列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常,先發(fā)送事件給應用程序的主窗口(keyWindow),主窗又會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件录择。關鍵來了崭孤,UIWindows首先判斷自己是否接收觸摸事件,及觸摸點是否在自己身上糊肠,這兩點滿足后,便開始從后往前遍歷子控件遗锣,注意此時 UIWindow 的子控件只有白色 View货裹,然后判斷白色 View 是否滿足 1.自己是否接收觸摸事件 2.觸摸點是否在自己身上,兩點滿足后開始執(zhí)行 3.從后往前遍歷子控件精偿。此時白色 View 有兩個子控件弧圆,綠色 View 和橙色 View赋兵,按照從后往前,即后添加的橙色 View 搔预。先判斷橙色 View 的是否滿足三個條件霹期,橙色判斷完后在判斷綠色,綠色判斷的時候不滿足觸摸點在自己身上拯田,終止历造。橙色判斷滿足前兩條,然后開始第三條船庇,遍歷橙色的子控件吭产,然后紅色不滿足,藍色繼續(xù)判斷鸭轮,前兩條滿足臣淤,然后遍歷藍色的子控件,只有黃色 View窃爷,此時黃色滿足2條邑蒋,但是沒有子控件,循環(huán)停止按厘,最終返回的就是黃色 View医吊。可以看出刻剥,如果父控件不能接收觸摸事件遮咖,觸摸事件是不能傳遞到子控件的。
底層實現(xiàn)
兩個方法
- hitTest:withEvent:方法
- pointInside方法
hitTest:withEvent:
方法用來尋找并返回最合適的 View造虏,可以通過重寫該方法返回指定的 View 作為最合適的 View御吞。這樣可以攔截事件的傳遞過程,指定 View 來處理事件漓藕。
事件傳遞給誰就會調用誰的hitTest:withEvent:
方法陶珠,如果hitTest:withEvent:
方法中返回nil
,那么調用該方法的控件本身和其子控件都不是最合適的 view享钞,也就是在自己身上沒有找到更合適的 view揍诽。那么最合適的 view 就是該控件的父控件。
事件傳遞給窗口或控件后栗竖,首先要調用hitTest:withEvent:
方法來尋找最合適的 View暑脆。其過程是先傳遞事件,view 接收到該事件后開始調用hitTest:withEvent:
方法狐肢。假如層級結構A->B->C添吗,A是最頂層的 View,A收到事件后份名,會根據(jù)hitTest:withEvent:
來查找最合適的 View碟联,此時會返回自己為合適的 view妓美,要查找最合適的 View 就需要繼續(xù)傳遞事件,即使 A 是最合適的 View,也需要進一步傳遞事件鲤孵,因為此時還不知道自己是最合適的 View壶栋,將事件傳遞給 B 之后,B 就會調用自己的 hitTest:withEvent:
方法來查找最合適的 View普监,仍然返回自己為合適的 View贵试,繼續(xù)尋找最合適的 View,然后事件傳遞給 C鹰椒,C 調用后有子控件锡移,則 C 就是最合適的 View。
想讓誰成為最合適的view就重寫誰自己的父控件的hitTest:withEvent:方法返回指定的子控件漆际。
hit:withEvent:方法底層會調用pointInside:withEvent:方法判斷點在不在方法調用者的坐標系上淆珊。
UIView 不接收觸摸實踐的三種情況
- 不支持交互
userInteractionEnabled = NO
- 隱藏
hidden = YES
- 透明度
alpha = 0.0 - 0.01
事件的響應
上述的事件傳遞,只是根據(jù) View 是否接收觸摸事件及觸摸點的位置來找到最合適的 View 來處理事件奸汇,找到最合適的 View 后會調用 touches 等方法來作具體的事件處理施符,但是最合適的 View 可能不具備 touches 方法來處理事件,此時機會存在一個事件響應的問題擂找,最合適的 View 不一定能響應該事件戳吝,引出了響應鏈的傳遞過程,一般是講事件交給上一個響應者來進行處理贯涎。
響應者鏈條示意圖
- 響應者鏈條: 是由多個響應者對象連接起來的鏈條
- 作用:可以很清楚的看見每個響應者之間的聯(lián)系听哭,并且可以讓一個事件處理給多個對象處理
- 響應者對象:能處理事件的對象
事件傳遞的完整過程
- 先將事件對象由上往下傳遞(由父控件傳遞給子控件),找到最合適的控件 來處理這個事件。
- 調用最合適控件的touches....方法
- 如果調用了
[super touches....]
;就會將事件順著響應者鏈條往上傳遞,傳遞
給上一個響應者 - 接著就會調用上一個響應者的touches....方法
如何判斷上一個響應者
- 如果當前這個view是控制器的view,那么控制器就是上一個響應者
- 如果當前這個view不是控制器的view,那么父控件就是上一個響應者
響應者鏈的事件傳遞過程
- 如果view的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它 的父視圖
- 在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件 或消息傳遞給window對象進行處理
- 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
- 如果UIApplication也不能處理該事件或消息,則將其丟棄
事件的傳遞和響應
- 當一個事件發(fā)生后首先發(fā)生的是事件的傳遞塘雳,事件會從父控件依次傳給子控件陆盘,Application -> UIWindow -> UIView -> initial view,此過程是尋找最合適的 view 的過程
- 找到最合適的 view 后败明,就是事件的響應過程隘马,先看 initial view 是否能夠處理事件,如果不能將傳給上級視圖妻顶,傳到視圖控制器酸员,一直傳到 window 和 application,都不能處理則丟棄
- 事件的響應中讳嘱,如果某個控件實現(xiàn)了
touches
方法幔嗦,則這個事件將由該控件來處理,如果調用了[supertouches….]
;就會將事件順著響應者鏈條往上傳遞沥潭,傳遞給上一個響應者崭添;接著就會調用上一個響應者的touches
方法。 - 通過重寫自己的
touches
方法和父控件的touches
方法可以達到一個事件多個對象處理的目的
** 事件的傳遞和響應的區(qū)別:**
- 事件的傳遞是從上到下(父控件到子控件)
- 事件的響應是從下到上(順著響應者鏈條向上傳遞:子控件到父控件叛氨。
Demo
關于事件傳遞和響應鏈條的小Demo:事件傳遞及響應Demo