事件
在iOS中事件UIEvent主要分為以下3大類:
-
觸摸事件
: 手指觸摸屏幕,點(diǎn)擊,滑動(dòng)等 -
加速計(jì)事件
: 主要由傳感器產(chǎn)生,如微信的搖一搖 -
遠(yuǎn)程控制事件
: 遠(yuǎn)程事件,比如通過藍(lán)牙耳機(jī)切換歌曲
本文以觸摸事件為例,介紹事件的產(chǎn)生,傳遞和響應(yīng).
事件的產(chǎn)生
當(dāng)我們觸摸屏幕時(shí),系統(tǒng)會(huì)自動(dòng)生成一個(gè)觸摸事件并加入到一個(gè)由
UIApplication
管理的事件隊(duì)列
中.由于隊(duì)列先進(jìn)先出的特性,
UIApplication
會(huì)從事件隊(duì)列中取出最前面的事件
儿礼,并將事件分發(fā)下去以便處理珊豹,通常跨嘉,先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow
)职烧。
事件的傳遞
問題: Application 如何知道事件應(yīng)該傳遞給哪個(gè)響應(yīng)者(UIResponder)?
答: 通過Hit-testing
查找事件的最佳響應(yīng)者
Hit-testing(重點(diǎn))
Hit-testing
使用反向順序的深度優(yōu)先遍歷,首先從根視圖開始
,從父視圖到子視圖遍歷,一旦找到了包含觸摸點(diǎn)的最后一個(gè)視圖,便把這個(gè)視圖作為最佳響應(yīng)者
.
下面這張圖顯示了視圖層次結(jié)構(gòu)及在屏幕上繪制的UI的示例即横。
可以看出站楚,View A
和View B
及其子級(jí)View A.2
和View B.1
重疊蝠引。但是单起,由于View B
比View A
更晚添加到MainView上,因此View B
及其子視圖呈現(xiàn)在View A
及其子View 上劣坊。因此嘀倒,當(dāng)用戶的手指在與View A.2
重疊的區(qū)域觸摸View B.1
時(shí),應(yīng)通過Hit-testing
返回View B.1
局冰。如下圖:
遍歷算法首先將hitTest:withEvent:
消息發(fā)送到UIWindow
测蘑,這是視圖層次結(jié)構(gòu)的根視圖。這個(gè)方法的返回值是最佳響應(yīng)者
.
以下流程圖是Hit-testing
的實(shí)現(xiàn)邏輯邏輯康二。
以下代碼可能是
hitTest:withEvent:
的實(shí)現(xiàn)方式.(因?yàn)樘O果不開源,所以我們只能自己推斷)
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 {
return nil
}
if self.point(inside: point, with: event) {
for subview in subviews {
let convertedPoint = subview.convert(point, from: self)
if let hitTestView = subview .hitTest(convertedPoint, with: event) {
return hitTestView
}
}
return self
}
return nil
}
這個(gè)方法首先判斷是否允許視圖接收事件
碳胳。在這幾種情況下,允許視圖接收事件:
- 該視圖未隱藏:
hidden == NO
- 該視圖已開啟用戶交互:
userInteractionEnabled == YES
- 該視圖的透明度大于0.01:
alpha > 0.01
- 該視圖包含觸摸點(diǎn):
pointInside:withEvent: == YES
如果允許視圖接收事件,則此方法倒序遍歷子視圖(subviews
),并給每個(gè)子視圖發(fā)送hitTest:withEvent:
消息,直到其中一個(gè)返回非nil值沫勿。如果所以子視圖返回nil,或者沒有子視圖挨约,則返回self。如果不允許視圖接收事件产雹,則此方法將返回nil诫惭,而不會(huì)遍歷子視圖。因此蔓挖,Hit-testing
過程可能不會(huì)訪問視圖層次結(jié)構(gòu)中的所有視圖夕土。
當(dāng)找到最佳響應(yīng)者后,application
會(huì)把事件傳遞給最佳響應(yīng)者,也就是UIApplication->window->處理事件最合適的view
.
事件的處理
在iOS中不是任何對象都能處理事件,只有繼承了UIResponder
的對象才能接受并處理事件瘟判,我們稱之為響應(yīng)者對象
怨绣。以下都是繼承自UIResponder
的角溃,所以都能接收并處理事件。
- UIApplication
- UIViewController
- UIView
問題:那么為什么繼承自UIResponder的類就能夠接收并處理事件呢篮撑?
答:因?yàn)閁IResponder中提供了以下4個(gè)對象方法來處理觸摸事件减细。// 觸摸事件 - (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 // 加速計(jì)事件 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event // 遠(yuǎn)程控制事件 - (void)remoteControlReceivedWithEvent:(UIEvent *)event
以UIView為例來說明觸摸事件的處
// UIView是UIResponder的子類,可以覆蓋下列4個(gè)方法處理不同的觸摸事件
// 一根或者多根手指開始觸摸view咽扇,系統(tǒng)會(huì)自動(dòng)調(diào)用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移動(dòng)邪财,系統(tǒng)會(huì)自動(dòng)調(diào)用view的下面方法(隨著手指的移動(dòng),會(huì)持續(xù)調(diào)用該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指離開view质欲,系統(tǒng)會(huì)自動(dòng)調(diào)用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 觸摸結(jié)束前树埠,某個(gè)系統(tǒng)事件(例如電話呼入)會(huì)打斷觸摸過程,系統(tǒng)會(huì)自動(dòng)調(diào)用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch對象
需要注意的是:以上四個(gè)方法是由系統(tǒng)自動(dòng)調(diào)用的嘶伟,所以可以通過重寫該方法來處理一些事件怎憋。
- 如果兩根手指同時(shí)觸摸一個(gè)view,那么view只會(huì)調(diào)用一次touchesBegan:withEvent:方法九昧,touches參數(shù)中裝著2個(gè)UITouch對象
- 如果這兩根手指一前一后分開觸摸同一個(gè)view绊袋,那么view會(huì)分別調(diào)用2次touchesBegan:withEvent:方法,并且每次調(diào)用時(shí)的touches參數(shù)中只包含一個(gè)UITouch對象
- 重寫以上四個(gè)方法铸鹰,如果是處理UIView的觸摸事件,必須要自定義UIView子類繼承自UIView癌别。
- 如果是處理UIViewController的觸摸事件,那么在控制器的.m文件中直接重寫那四個(gè)方法即可蹋笼。
問題: 如果響應(yīng)者沒有處理接受到的事件會(huì)怎么樣呢展姐?
答:通過響應(yīng)者鏈條
找到上一個(gè)事件響應(yīng)者,讓它處理剖毯。
響應(yīng)者鏈條
在iOS程序中無論是最后面的UIWindow還是最前面的某個(gè)按鈕圾笨,它們的擺放是有前后關(guān)系的,一個(gè)控件可以放到另一個(gè)控件上面或下面逊谋,那么用戶點(diǎn)擊某個(gè)控件時(shí)是觸發(fā)上面的控件還是下面的控件呢擂达,這種先后關(guān)系構(gòu)成一個(gè)鏈條就叫“響應(yīng)者鏈”。也可以說胶滋,響應(yīng)者鏈?zhǔn)怯啥鄠€(gè)響應(yīng)者對象連接起來的鏈條板鬓。在iOS中響應(yīng)者鏈的關(guān)系可以用下圖表示:
響應(yīng)者鏈的事件傳遞過程:
- 如果當(dāng)前view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者镀钓,事件就傳遞給控制器穗熬;如果當(dāng)前view不是控制器的view,那么父視圖就是當(dāng)前view的上一個(gè)響應(yīng)者丁溅,事件就傳遞給它的父視圖
- 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖唤蔗,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進(jìn)行處理
- 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
- 如果UIApplication也不能處理該事件或消息妓柜,則將其丟棄
總結(jié)
- 用戶點(diǎn)擊屏幕后產(chǎn)生的一個(gè)觸摸事件箱季,通過
Hit-testing
,會(huì)找到最合適的視圖控件來處理這個(gè)事件
- 用戶點(diǎn)擊屏幕后產(chǎn)生的一個(gè)觸摸事件箱季,通過
- 找到最合適的視圖控件后棍掐,就會(huì)調(diào)用控件的·touches·方法來作具體的事件處理
touchesBegan
…touchesMoved
…touchedEnded
…
- 找到最合適的視圖控件后棍掐,就會(huì)調(diào)用控件的·touches·方法來作具體的事件處理
- 這些touches方法的默認(rèn)是將事件順著響應(yīng)者鏈條向上傳遞(
也就是touch方法默認(rèn)不處理事件藏雏,只傳遞事件
),將事件交給上一個(gè)響應(yīng)者進(jìn)行處理
- 這些touches方法的默認(rèn)是將事件順著響應(yīng)者鏈條向上傳遞(
- 如果沒有響應(yīng)者處理該事件作煌,則傳遞到UIApplication后會(huì)將事件丟棄掘殴。
問題:如果給一個(gè)視圖添加了點(diǎn)擊手勢,又實(shí)現(xiàn)touchesBegan方法粟誓,那么由誰來處理事件奏寨?
答:根據(jù)事件處理優(yōu)先級(jí),由手勢響應(yīng)處理事件.但是視圖
也能接受到touchesBegan
事件,因?yàn)槭謩葑R(shí)別是需要一點(diǎn)時(shí)間的,在手勢還是Possible 狀態(tài)的時(shí)候事件傳遞給了響應(yīng)鏈的第一個(gè)響應(yīng)對象,也就是我們的視圖
.
注意: 手勢處理完事件之后會(huì)調(diào)用touchesCancelled
方法廢棄掉事件.如果視圖是一個(gè)Button
,那么這個(gè)Button
的addTarget:action
方法是接收不到事件的.
事件處理優(yōu)先級(jí)
- 手勢: 處理完事件之后會(huì)調(diào)用
touchesCancelled
方法廢棄掉事件.
- 手勢: 處理完事件之后會(huì)調(diào)用
- toucheBegan: 由于手勢處理需要時(shí)間,能在處理手勢之前接收到事件.可以調(diào)用
[super toucheBegan:]
方法將事件繼續(xù)傳遞給addTarget:action:
- toucheBegan: 由于手勢處理需要時(shí)間,能在處理手勢之前接收到事件.可以調(diào)用
- addTarget:action: 如果添加了
手勢
或?qū)崿F(xiàn)了touchesBegan
方法則接收不到事件.
- addTarget:action: 如果添加了