在iOS中UIResponder類是專門用來響應(yīng)用戶的操作處理各種事件的,包括觸摸事件(Touch Events)、運(yùn)動(dòng)事件(Motion Events)、遠(yuǎn)程控制事件(Remote Control Events,如插入耳機(jī)調(diào)節(jié)音量觸發(fā)的事件)。我們知道UIApplication、UIView、UIViewController這幾個(gè)類是直接繼承自UIResponder,UIWindow是直接繼承自UIView的一個(gè)特殊的View,所以這些類都可以響應(yīng)事件。當(dāng)然我們自定義的繼承自UIView的View以及自定義的繼承自UIViewController的控制器都可以響應(yīng)事件。iOS里面通常將這些能響應(yīng)事件的對(duì)象稱之為響應(yīng)者。
下面我們根據(jù)UIResponder.h頭文件來具體介紹關(guān)于響應(yīng)事件的各個(gè)方面。
一. UIResponder類
這個(gè)部分我們主要介紹3種事件類型,即觸摸事件,運(yùn)動(dòng)事件,遠(yuǎn)程控制事件。當(dāng)用戶觸發(fā)某一事件時(shí),UIKit會(huì)創(chuàng)建一個(gè)UIEvent事件對(duì)象(關(guān)于iOS事件對(duì)象可以參考這篇文章),事件對(duì)象會(huì)加入到一個(gè)FIFO先進(jìn)先出的隊(duì)列中,UIApplication對(duì)象處理事件時(shí),會(huì)從隊(duì)列頭部取出一個(gè)事件對(duì)象進(jìn)行分發(fā)。
1.觸摸事件
@interface UIResponder : NSObject
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;//觸摸屏幕
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;//在屏幕上移動(dòng)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;//離開屏幕
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;//系統(tǒng)事件干擾
這4個(gè)方法是觸摸事件的最原始處理的4個(gè)方法,分別代表觸摸屏幕,在屏幕上移動(dòng),離開屏幕以及受到系統(tǒng)優(yōu)先級(jí)別高的事件的干擾(比如來電話)取消觸摸事件。另外UIKit框架對(duì)于觸摸事件為我們提供了UIGestureRecognizer
手勢(shì)識(shí)別這個(gè)類,基本上能滿足我們的大部分需求(可以參考這篇文章)。這里介紹的是最底層的處理方法,比如可以用來實(shí)現(xiàn)繪圖類型的APP.
上面提到UIApplication對(duì)象從隊(duì)列中取出事件對(duì)象進(jìn)行分發(fā),對(duì)于觸摸事件來說,UIApplication會(huì)首先把事件交給keyWindow,Window會(huì)將事件交給UIGestureRecognizer
處理,如果UIGestureRecognizer
識(shí)別了傳遞過來的事件,則交給相對(duì)應(yīng)的target去處理(關(guān)于iOS手勢(shì)事件可以參考這篇文章),事件不會(huì)再傳遞,如果UIGestureRecognizer
并沒有識(shí)別傳遞過來的事件(可能是沒有視圖添加手勢(shì),也可能手勢(shì)識(shí)別不成功),事件會(huì)傳遞到視圖樹形結(jié)構(gòu),會(huì)分成尋找接受者和事件響應(yīng)這兩個(gè)步驟。
1.在iOS視圖樹形結(jié)構(gòu)中找到最終的接收者,也就是觸摸事件發(fā)生的那個(gè)最上層的View上,這一過程稱為hit-testing(測(cè)試命中),通過一層層的遍歷找到最終的命中視圖稱為hit-test view.
UIView中有兩個(gè)方法用來確定hit-test view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
這是一張我從apple官方文檔里面的截圖,這里所有的顯示的View都是加載到主window上,假設(shè)我們觸摸到屏幕上ViewD的區(qū)域,當(dāng)我們沒有重載UIView的
hitTest:withEvent:
和pointInside:withEvent:
這兩個(gè)方法時(shí),系統(tǒng)默認(rèn)的處理如下:
- keyWindow調(diào)用
pointInside:withEvent:
判斷觸摸點(diǎn)是否在其frame范圍內(nèi),返回Yes,遍歷keyWindow的subView->ViewA. - ViewA調(diào)用
pointInside:withEvent:
判斷觸摸點(diǎn)是否在其frame范圍內(nèi),返回Yes,遍歷ViewA的subView->ViewB、ViewC(關(guān)于ViewB和ViewC先執(zhí)行哪個(gè),是根據(jù)ViewA添加子控件的先后順序,總是先執(zhí)行后添加的subView.假設(shè)添加ViewB后添加ViewC) - ViewC調(diào)用
pointInside:withEvent:
判斷觸摸點(diǎn)是否在其frame范圍內(nèi),返回Yes,遍歷ViewC的subView->ViewD、ViewE - ViewE調(diào)用
pointInside:withEvent:
判斷觸摸點(diǎn)是否在其frame范圍內(nèi),返回NO,ViewE的hitTest:withEvent:
返回nil(如果是先執(zhí)行ViewB的情況,假設(shè)ViewB還有子節(jié)點(diǎn)subView,由于ViewB的pointInside:withEvent:
返回NO,ViewB的hitTest:withEvent:`直接返回nil是不會(huì)再去遍歷ViewB的子節(jié)點(diǎn)的) - ViewD調(diào)用
pointInside:withEvent:
判斷觸摸點(diǎn)是否在其frame范圍內(nèi),返回Yes并且沒有子節(jié)點(diǎn)subView,ViewD的hitTest:withEvent:
返回ViewD本身,即為最終的hit-test view(不會(huì)再遍歷ViewB)iewB
需要注意的是:View.isHidden=YES View.alpha<=0.01 View.userInterfaceEnable=NO View.enable = NO(指繼承自UIControl的View)的這4種情況下,View的pointInside返回NO,hitTest方法返回nil
默認(rèn)UIImageView
的userInterfaceEnable=NO
2.找到了hit-test view,下一個(gè)步驟就是響應(yīng)事件。說明一下,對(duì)于觸摸事件來說,無論View是否處理事件,即使是application通過[application beginIgnoringInteractionEvents]
忽略了觸摸事件,上面hit-testing的過程依然存在,它只影響第二個(gè)步驟事件響應(yīng)的過程。下面我們將介紹iOS響應(yīng)者鏈條(Responder chain)
這是我從官方文檔里面截取的一張關(guān)于響應(yīng)者鏈條的截圖。我們先看上圖左邊的情況:標(biāo)注為①的地方即為步驟1找到的hit-test view 它作為第一響應(yīng)者來響應(yīng)這個(gè)事件,如果該view沒有通過重寫或者封裝touch系列方法來處理該事件,默認(rèn)touch的實(shí)現(xiàn)就是調(diào)用父類的touch方法,將事件傳遞下去。在這里由1->傳遞到它的父類2,2是控制器的根view,->傳遞到vc控制器->傳遞到窗口window->傳遞到application
再看上圖右邊的情況:標(biāo)注為①的地方即為步驟1找到的hit-test view,同時(shí)它是控制器的根view并且還有父視圖,事件傳遞到控制器->再傳遞到父視->傳遞到控制器,再傳遞到父視圖窗口->application。其實(shí)上圖左邊部分也可以理解為窗口是控制器根視圖的父視圖。如果整個(gè)響應(yīng)者鏈條結(jié)束,都沒有對(duì)事件做處理,那么該事件會(huì)被丟棄。
總結(jié)一下響應(yīng)者鏈條的傳遞過程是:由第一響應(yīng)者(對(duì)于觸摸事件來說是hist-test view)開始向上傳遞。如果該視圖是控制器的根視圖,先傳遞給控制器,再傳遞給父視圖,如果不是控制器的根視圖,直接傳遞給父視圖。
只要在響應(yīng)者的處理方法里面調(diào)用父類的方法,就可以讓多個(gè)視圖和控制器響應(yīng)同一個(gè)事件,響應(yīng)者鏈條的根本目的是:共享事件,讓多個(gè)視圖和控制器可以對(duì)同一事件做不同的處理。
2.運(yùn)動(dòng)事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
這3個(gè)方法是運(yùn)動(dòng)事件的最原始處理的3個(gè)方法,這里處理的運(yùn)動(dòng)事件特指shake事件,手機(jī)搖動(dòng)觸發(fā)手機(jī)內(nèi)部的加速度傳感器,可以用來實(shí)現(xiàn)搖一搖計(jì)算運(yùn)動(dòng)的步數(shù)等等應(yīng)用。類似于觸摸事件,這3個(gè)方法分別代表事件開始、事件結(jié)束和受到系統(tǒng)干擾取消事件。
加速度計(jì)accelerometer
實(shí)際上是由三個(gè)加速度計(jì)組成,分別用于測(cè)量X,Y和Z軸直線路徑速度的變化。結(jié)合所有三個(gè)加速度計(jì)可以檢測(cè)設(shè)備朝任何方向的運(yùn)動(dòng)和獲取設(shè)備的當(dāng)前方向。對(duì)于shake事件來說,我們不關(guān)心3個(gè)方向上的運(yùn)動(dòng),只作為一個(gè)事件對(duì)象來處理。如果只是處理設(shè)備的大方向,并不需要知道方向向量,如橫屏豎屏屏幕旋轉(zhuǎn),我們可以使用UIDevice類(參考文章)。如果我們需要知道3個(gè)方向上的運(yùn)動(dòng)做更細(xì)致化的處理,如上了多少層樓等運(yùn)動(dòng)類型APP,可以使用的核心運(yùn)動(dòng)框架訪問加速度計(jì),陀螺儀和設(shè)備的運(yùn)動(dòng)類來做處理(Core Motion參考文章)
上面介紹的響應(yīng)者鏈條對(duì)shake事件同樣適用,只不過,沒有hit-testing過程,如果當(dāng)前顯示的視圖界面沒有一個(gè)view聲明為第一響應(yīng)者(調(diào)用becomeFirstResponder
申明并且View需要重寫canBecomeFirstResponder
方法返回YES,默認(rèn)返回為NO),默認(rèn)當(dāng)前視圖控制器為第一響應(yīng)者,并將事件沿著響應(yīng)者鏈條傳遞,直到被處理。如果有視圖聲明為第一響應(yīng)者,就從該視圖開始傳遞事件直到被處理,如果該事件最終沒有被處理并且UIApplication的applicationSupportsShakeToEdit
屬性為YES(默認(rèn)就是YES),當(dāng)鍵盤顯示的時(shí)候,系統(tǒng)會(huì)有一個(gè)是否撤銷正在輸入的警告。就是微信和QQ上在輸入的時(shí)候搖動(dòng)手機(jī)提示撤銷輸入的那種效果。關(guān)于更多撤銷方面的操作參考NSUndoManager
3.遠(yuǎn)程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);
遠(yuǎn)程控制事件一般用于多媒體事件,播放暫停上一曲下一曲快進(jìn)快退等。