術語
? ? ? ?hit-testing ? ? ? ? ?點擊檢測
? ? ? ?responder chain ? 響應鏈
響應鏈可以做到的事情
通過相應鏈榴嗅,我們可以改變用戶操作的響應順序
可以查找到視圖的響應控制器
事件流程
當用戶觸發(fā)的一個事件發(fā)生靶瘸,UIKit會創(chuàng)建一個包含要處理的事件信息的事件對象。然后她會將事件對象放入active app’s(應用程序對象江咳,每個程序對應唯一一個)事件隊列(為什么是隊列而不是棧叮喳?因為隊列是先進先出睁宰,用于保證先產生的事件先處理,棧是先進后出)嘱么。對于觸摸事件狮含,事件對象就是UIEvent對象封裝的一系列觸摸集合。對于動作事件拱撵,這個事件對象依賴于使用的framework和你關心哪種動作事件辉川。
事件通過特殊的路徑傳遞直到被傳遞到一個可以處理該事件的對象。首先拴测,單例的UIApplication對象從頂層的隊列中獲取事件乓旗,然后分發(fā)。典型的集索,它將事件發(fā)送到App的關鍵window(key window)對象屿愚,window則為了處理該事件而發(fā)送它到初始化對象(initial object),這個初始化對像依靠事件類型务荆。
初始化對象查找后的反饋過程如下圖
For the app on the left, the event follows this path:
The initial view attempts to handle the event or message. If it can’t handle the event, it passes the event to its superview, because the initial view is not the top most view in its view controller’s view hierarchy.
The superview attempts to handle the event. If the superview can’t handle the event, it passes the event to its superview, because it is still not the top most view in the view hierarchy.
The topmost view in the view controller’s view hierarchy attempts to handle the event. If the topmost view can’t handle the event, it passes the event to its view controller.
The view controller attempts to handle the event, and if it can’t, passes the event to the window.
If the window object can’t handle the event, it passes the event to the singleton app object.
If the app object can’t handle the event, it discards the event.
The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:
A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.
The topmost view passes the event to its view controller.
The view controller passes the event to its topmost view’s superview.
Steps 1-3 repeat until the event reaches the root view controller.
The root view controller passes the event to the window object.
The window passes the event to the app object.
左邊的情況妆距,最初的視圖接收到事件,如果它不能處理函匕,那么它將會傳遞給它的父試圖娱据,它的父試圖也不能處理,再傳遞給父試圖的父試圖盅惜,如果還不能處理中剩,再傳遞給ViewController,也不能處理抒寂,傳遞給window结啼,也不能處理,傳遞給Application屈芜。
右邊的情況郊愧,最初的視圖它不能處理時會將事件傳遞給它的視圖控制器朴译,視圖控制器會將事件會將事件傳遞給視圖控制器的視圖的父試圖,然后重復這樣的過程属铁,直到根視圖控制器眠寿,也不能處理,傳遞給window红选,也不能處理澜公,傳遞給Application
蘋果說兩種app設置的原因,但是基本都是如果初始化對象(initial object)—— 即hit-test view或者first responder —— 不處理事件喇肋,UIKit會將事件傳遞給responder chain的下一個responder坟乾。每個responder決定它是傳遞事件還是通過nextResponder方法傳遞給它的下一個responder。這個操作繼續(xù)直到一個responder處理event或者沒有responder了蝶防。
總而言之:
事件的傳遞和響應分兩個鏈:
傳遞鏈:由系統(tǒng)向離用戶最近的view傳遞甚侣。UIKit –> active app’s event queue –> window –> root view –>……–>lowest view
響應鏈:由離用戶最近的view向系統(tǒng)傳遞。initial view –> super view –> …..–> view controller –> window –> Application(如果view是控制器的view间学,就傳遞給控制器殷费;如不是,則將其傳遞給它的父視圖 在視圖層次結構的最頂級視圖低葫,如果也不能處理收到的事件或消息详羡,則其將事件或消息傳遞給window對象進行處理 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象 如果UIApplication也不能處理該事件或消息嘿悬,則將其丟棄)
上面是原理实柠,下面說方法:
- (UIView * _Nullable)hitTest:(CGPoint)point withEvent:(UIEvent * _Nullable)event
此方法會在視圖層級結構中的每個視圖上調用
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event
如果pointInside:withEvent:返回YES,則繼續(xù)逐級調用,直到找到touch操作發(fā)生的位置善涨,這個視圖也就是hit-test view窒盐。
hitTest:withEvent:方法的處理流程如下:
首先調用當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內;
若返回NO,則hitTest:withEvent:返回nil;
若返回YES,則向當前視圖的所有子視圖(subviews)發(fā)送hitTest:withEvent:消息钢拧,所有子視圖的遍歷順序是從top到bottom蟹漓,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;
若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象源内,處理結束葡粒;
如所有子視圖都返回非,則hitTest:withEvent:方法返回自身(self)膜钓。
hitTest:withEvent:忽略的三種情況:
1:hidden = YES 的視圖
2:userInteractionEnabled = YES 的視圖
3:alpha < 0.01 的視圖
如果一個子視圖的區(qū)域超過父視圖的bound區(qū)域(父視圖的clipsToBounds 屬性為NO,這樣超過父視圖bound區(qū)域的子視圖內容也會顯示)塔鳍,那么正常情況下對子視圖在父視圖之外區(qū)域的觸摸操作不會被識別,因為父視圖的pointInside:withEvent:方法會返回NO,這樣就不會繼續(xù)向下遍歷子視圖了。當然呻此,也可以重寫pointInside:withEvent:方法來處理這種情況。(objc_setAssociatedObject添加屬性腔寡,擴展響應區(qū)域)
如果父視圖需要對對哪個子視圖可以響應觸摸事件做特殊控制焚鲜,則可以重寫hitTest:withEvent:或pointInside:withEvent:方法。
如button,scrollview同為topView的子視圖,但scrollview覆蓋在button之上忿磅,這樣在在button上的觸摸操作返回的hit-test view為scrollview,button無法響應糯彬,可以修改topView的hitTest:withEvent:方法如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? ? UIView *result = [super hitTest:point withEvent:event];
? ? ? CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
? ? ? if ([underButton pointInside:buttonPoint withEvent:event]) {
? ? ? ? ? return underButton;
? ? ? ?}
? ? ? return result;
}