在 iOS 開發中,當用戶用手指點擊了一下屏幕,會發生什麼呢?系統是怎麼判斷用戶點擊的位置呢?咱們開發者又如何作出「沒有bug」的交互呢?帶着這些疑問,咱們一塊兒談談事件的分發與響應。git
顧名思義,事件就是發生的一件事,對於APP來講,就是發生的一個操做。具體的就是用戶點擊一下屏幕就會出現一個事件(體現爲一個UIEvent
),即一個觸摸事件。其實,對於 iOS 設備的用戶來講,他們操做設備的方式主要有四種方式:觸摸屏幕、晃動設備、經過遙控設施控制設備、按壓屏幕。 對應的事件類型UIEventType
有如下三種:github
咱們的主題是探索用戶用手指點擊屏幕會發生什麼,因此咱們將注意力放在觸摸事件
上。數組
上面咱們瞭解到,當咱們點擊了屏幕,就會出現一個事件。既然事件出現了,那麼就須要一個一個響應和處理這個事件的對象,那就是咱們的響應者對象。這些響應者對象都有一個共同的特徵,就是他們都繼承自UIResponder
。咱們熟知的響應者對象有UIApplication
、UIWindow
、 UIViewController
和全部繼承自UIView
的 UIKit 類bash
UIResponderide
在觸摸屏幕的事件中:測試
上面介紹了響應者對象,也知道了UIApplication
、UIWindow
、 UIViewController
、UIView
這些都是響應者。那麼一個 APP 會存在不少響應者對象。由這一系列的響應者對象就構成了一個層次結構,那就是響應者鏈條。ui
從上圖中能夠看到,響應者鏈條有如下特色:spa
UIView
)構成的;UIViewController
)的,那麼下一個響應者是該視圖控制器,而後再將事件響應到它的父視圖(Super View
)中;UIViewController
),那麼下一個響應者就直接是它的父視圖(Super View
);UIWindow
)UIApplication
),也是響應者鏈條的終點回到開篇的狀況,當用戶點擊了一下屏幕。系統檢測到用戶的觸摸事件,就會將其打包成一個事件(即UIEvent
對象),並將這個UIEvent
對象放入 Application 的事件隊列中。這時系統只是知道有這麼一個事件發生,雖然響應者鏈條中有不少有處理事件能力的響應者,可是它不知道誰纔是響應這個事件的最佳人選。 所以,系統會從UIApplication
開始,順着響應者鏈條向上尋找那個最佳的人選。這個尋找的過程就是事件的分發過程。code
UIApplication
將這個事件從事件隊列中拿出來,從頂部開始詢問誰纔是最佳人選;UIWindow
會最早獲取到事件,並開始使用hitTest:withEvent:
來判斷下面他的子控件中誰纔是最佳人選;UIView
繼續詢問他的子視圖是否是最佳人選;UIView
不是被點擊的的視圖,orz,上一個UIView
就是最佳人選了。從用戶視角來看,系統經過hitTest:withEvent:
方法,從視圖的底部一直向表面尋找最佳人選。由於是一直查找,只有在全部的查找都完成了,判斷出當前視圖沒有子視圖或者他的子視圖都不適合了,那麼當前視圖就是最佳人選了。(因此你只是點了一個你一眼就看中的視圖,其實系統是從底部開始,一頓連續操做才找到你想要的東西[汗顏])cdn
上面的事件分發過程當中,大量使用了hitTest:withEvent:
這個方法,它的處理流程以下:
pointInside:withEvent:
方法,判斷觸摸點是否在當前視圖內;
NO
,則hitTest:withEvent:
返回nil
;YES
,則向當前視圖的全部子視圖發送hitTest:withEvent:
消息,全部子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews
數組的末尾向前遍歷。hitTest:withEvent:
方法返回此對象,處理結束;nil
,則hitTest:withEvent:
方法返回自身,即self
,處理結束。下面咱們用一個圖解來理解一下這個hitTest:withEvent:
:
假如用戶點擊了View D
,結合上圖詳細介紹一下hitTest:withEvent:
過程: (hitTest:withEvent:
簡稱hitTest
,pointInside:withEvent:
簡稱pointInside
,View X
簡稱X
)
hitTest
;hitTest
。
hitTest:withEvent:
返回 nil;hitTest
時返回了 nil);hitTest
會將 D 返回,再往回回溯,就是 C 的hitTest
返回 D,A 的hitTest
返回 D。至此,本次點擊事件的第一響應者就經過響應者鏈的事件分發邏輯成功找到了
除了使用pointInside:withEvent:
判斷是不是響應者,還有下面三種狀況會使hitTest:withEvent:
返回 nil:
hidden=YES
的視圖;userInteractionEnabled=YES
的視圖;alpha<0.01
的視圖。所以hitTest:withEvent:
的實現多是:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
複製代碼
前面說了一大堆事件的分發,其實就是爲了找到響應事件的最佳人選,這個最佳人選就是在介紹響應者鏈條的時候,最底下的那個View。從這個 View 開始咱們沿着響應者鏈條的方向進行響應。
開篇咱們的說的是用戶點擊屏幕的場景,所以,響應者會按照當前UITouch
的所處階段使用下面的方法進行響應:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
複製代碼
[super touches...]
將這個觸摸事件繼續分發給父控件的對應方法處理。而後父控件還能夠將該事件繼續向上傳遞,直到傳遞給UIApplication對象。這一系列的響應者對象就構成了一個響應者鏈條。[super toucher...]
事件不會繼續沿着響應者鏈條進行響應事件的分發和響應都是在響應者鏈條上進行的,只不過是二者傳遞的方向不一樣。
UIViewController
,這裏說明一下他的位置:
ViewController
的事至此,咱們已經大概瞭解了當用戶用手指點擊了一下屏幕,會發生什麼。
經過對這些的瞭解,咱們能夠經過使用下面兩種方式來實現一些特殊需求:
- 重寫 UIView 中的
hitTest:withEvent:
來影響事件分發- 重寫 UIResponder 中的
touches
系列方法來影響事件響應
我在測試hitTest:withEvent:
的過程當中,經過運行時給每一個hitTest:withEvent:
都添加了打印方法,在點擊綠色的B View的時候出現了下面的重複尋找的狀況(不單隻點擊B View時候有出現)
這個現象我不太會解釋...但願有人能夠解答一下。