概述
本文主要解析從我們的手指觸摸蘋果設備到最終響應事件的整個處理機制。本質上講,整個過程可以分為兩個步驟:
步驟1:找目標。在iOS視圖層次結構中找到觸摸事件的最終接受者;
步驟2:事件響應。基於iOS響應者鏈(Responder Chain)處理觸摸事件
找目標
在找目標階段所使用到的兩大利器是UIView的 hitTest:withEvent: 以及 pointInside:withEvent: 方法。找目標的過程也稱為hit-Testing。先來看一張圖(注: 圖來自MJ)比較直觀:
下面解釋一下處理原理:
1、手指觸摸屏幕,這個動作被包裝成一個UIEvent對象發送給當前活躍的UIApplication (Active Application),Application將該Event對象插到任務隊列的末尾等待處理(先進先出,先來的先處理);
2、UIApplication單例將事件發送給APP的主Window(所有顯示的view都添加在Window上);
3、主Window調用視圖層次結構上逐級使用hit-Testing確認最終的響應目標,這個目標也稱為hitTesting view。
在沒有做任何重載操作的前提下,系統默認的hit-Testing的處理機制如下:
當前view調用自身的pointInside: withEvent:方法判斷觸摸點是否在自己范圍內:
- 若pointInside: withEvent:方法返回NO,則說明觸摸點不在自己范圍內,則 當前view的hitTest: withEvent:方法返回nil,當前view上的所有subview都不做判斷。有點領導的意見一票否決的味道。
- 若pointInside: withEvent:方法返回YES,則說明觸摸點在自己的范圍內。但無法判斷是否在自己身上還是在subview的身上。此時,遍歷所有的subviews,對每個subview調用hitTest方法。這里要注意,遍歷的順序是從當前view的subviews數組的尾部開始遍歷。因此離用戶最近的上層的subview會優先被調用hitTest方法。
- 一旦hitTest方法返回非空的view,則被返回的view就是最終相應觸摸事件的view,尋找hitTesting view的階段到此結束,不再遍歷。
- 若當前view的所有subviews的hitTest方法都返回nil,則當前view的hitTest方法返回self作為最終的hitTesting view,處理結束。
以上就是第一階段尋找響應view的機制。這里我們結合一個具體的例子再過一遍(圖片引自技術哥的博客):
當用戶點擊ViewD所在的區域時會進行以下hit-Testing:
- ViewA的pointInside返回YES,因為觸摸點在其bounds內。遍歷ViewA的兩個subview;
- ViewB的pointInside返回NO,因為觸摸點不在其bounds內,ViewB的hitTest方法返回nil。而且發生一票否決,在ViewB上的所有subviews受到牽連將不再進行hit-Testing處理。ViewC的pointInside返回YES,因為觸摸點在其bounds范圍內,ViewC的hitTest方法返回默認處理,也就是 return [super hitTest:point withEvent:event]; 遍歷ViewC的兩個subview;
- ViewD的pointInside返回YES,因為觸摸點在其bounds范圍內,且ViewD沒有subview,因此hitTest方法返回其自己。hitTesting view找到,結束處理。
這里有幾點需要強調:
1、hitTest方法調用pointInside方法;
2、hit-Testing過程是從superView向subView逐級傳遞,也就是從層次樹的根節點向葉子節點傳遞;
3、遇到以下設置時,view的pointInside將返回NO,hitTest方法返回nil:
- view.isHidden=YES;
- view.alpah<=0.01;
- view.userInterfaceEnable=NO;
- control.enable=NO;(UIControl的屬性)
hit-Testing過程用代碼可以描述如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) { return nil; } BOOL inside = [self pointInside:point withEvent:event]; UIView *hitView = nil; if (inside) { NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator]; for (UIView *subview in enumerator) { hitView = [subview hitTest:point withEvent:event]; if (hitView) { break; } } if (!hitView) { hitView = self; } return hitView; } else { return nil; } }
事件響應
上一部分我們通過hit-Testing機制找到了hitTesting View,這個hitTesting View就是觸摸事件的響應者Responder。在iOS系統中,能夠響應並處理事件的對象稱之為Responder Object,而UIResponder是所有responder的最頂層基類。當hitTesting view做完自己該做的動作后,可以根據需要將消息傳給下一級響應者。那下一級響應者會是什么呢?這取決於iOS中的響應者鏈Responder Chain,如下圖所示:
- UIView的nextResponder屬性,如果有管理此view的UIViewController對象,則為此UIViewController對象;否則nextResponder即為其superview。
- UIViewController的nextResponder屬性為其管理view的superview.
- UIWindow的nextResponder屬性為UIApplication對象。
- UIApplication的nextResponder屬性為nil。
更具體的:
- 如果hit-test view或first responder不處理此事件,則將事件傳遞給其nextResponder處理,若有UIViewController對象則傳遞給UIViewController,傳遞給其superView。
- 如果view的viewController也不處理事件,則viewController將事件傳遞給其管理view的superView。
- 視圖層級結構的頂級為UIWindow對象,如果window仍不處理此事件,傳遞給UIApplication.
- 若UIApplication對象不處理此事件,則事件被丟棄。
了解響應者鏈有時候可以幫我解決一些實際問題。我舉個例子,我們知道,當提供給你一個ViewController你可以很容易得到它的view,一句代碼的事情:
viewWanted = someViewController.view;
但如果反過來呢?當給你一個view,讓你找到其所在的ViewController呢?這時候響應者鏈可以幫上忙了,代碼如下:
@implementation UIView (FindController) -(UIViewController*)parentController{ UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController*)responder; } responder = [responder nextResponder]; } return nil; } @end
寫在最后
這篇文章解析了iOS響應觸摸事件的機制。或許你現在找不到這個知識的應用點,但是一旦你理解了,可以幫助你實現一些特別的需求,比如點擊某個按鈕,響應的卻是另一個按鈕;穿透某個view點擊到view下面的view... 更有甚者,你可以用上面的知識解決不規則區域觸摸問題(看我之前的文章)、不添加任何view就能擴大控件的可觸摸區域等。天馬行空,任我翱翔!
=======================================================
原創文章,轉載請注明 編程小翁@博客園,郵件zilin_weng@163.com,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!
歡迎跳轉我的GitHub主頁,關注我的開源代碼。也歡迎你Star/Fork/Watch我的項目。
=======================================================