開場(chǎng)白
iOS開發(fā)這么多年或听,其實(shí)從來就沒關(guān)心過時(shí)間傳遞和響應(yīng)機(jī)制這么個(gè)事。當(dāng)我看到這篇文章史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇后笋婿,發(fā)現(xiàn)其中有很多東西可以細(xì)細(xì)品味一下的誉裆。
1.簡(jiǎn)述事件流程
整個(gè)事件傳遞和處理流程,簡(jiǎn)單概括為:
事件-事件傳遞到指定界面-找到可響應(yīng)的界面-響應(yīng)
我開始的理解誤區(qū)就是‘傳遞到指定界面’和‘可響應(yīng)界面’理解成同一個(gè)界面了缸濒,造成我在看上面的文章的時(shí)候足丢,有些混亂粱腻。其實(shí)這兩個(gè)可以是兩個(gè)界面。
例如:我在touchBegin一個(gè)view的時(shí)候斩跌,需求是view不響應(yīng)绍些,而superview響應(yīng)。而事件傳遞是傳遞到view中耀鸦。這種情況兩個(gè)view就是不相同的界面柬批。
2.事件傳遞
- 當(dāng)有用戶觸摸屏幕的時(shí)候產(chǎn)生事件,系統(tǒng)硬件進(jìn)程獲取到這個(gè)事件袖订,并處理封裝保存在系統(tǒng)中氮帐,由于系統(tǒng)硬件進(jìn)程和app進(jìn)程是兩個(gè)不同的進(jìn)程,所以使用進(jìn)程間的端口通信洛姑。
- 系統(tǒng)會(huì)將這個(gè)事件加入到UIApplication的事件管理隊(duì)列中上沐,事件從隊(duì)列中出隊(duì)后通常會(huì)發(fā)送給app的keywindow處理。
- keywindow會(huì)找到一個(gè)最適合的視圖去處理事件楞艾。也就是從super控件到子控件中参咙。
- 簡(jiǎn)單總結(jié):UIApplication->window->尋找處理事件最合適的view
2.1 找到適合視圖的過程
- 首先keywindow是可以接受事件的
- 判斷是否事件發(fā)生在自己的可視范圍內(nèi),例如:觸摸點(diǎn)擊在自己的bound中硫眯。
- 子控件數(shù)組按照從后往前的順序查找適合的子控件蕴侧,重復(fù)步驟1和步驟2。(從后往前的意思就是subviews中從最后一個(gè)元素開始向前找舟铜,這種方式可以減少遍歷次數(shù)戈盈,提高效率)
- 找到子控件后再繼續(xù)找它的子控件。
- 如果沒有找到合適的子控件谆刨,那么當(dāng)前的控件就是最適合的塘娶。
2.2 UIView不能接收觸摸事件的三種情況
- 不允許交互:userInteractionEnabled = NO,例如UIImageView中addSubview一個(gè)button痊夭,button的點(diǎn)擊是沒有反應(yīng)的刁岸。
- 隱藏:如果把父控件隱藏,那么子控件也會(huì)隱藏她我,隱藏的控件不能接受事件
- 透明度:如果設(shè)置一個(gè)控件的透明度<0.01虹曙,會(huì)直接影響子控件的透明度。
如果不想讓view處理事件番舆,而是想讓superview處理酝碳,就可以吧view的userInteractionEnabled設(shè)置為no。
2.3 最適合的子控件
系統(tǒng)api中提供了兩個(gè)方法恨狈,
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
為了方便:hitTest:withEvent:方法在文章后續(xù)用hitTest代替疏哗,pointInside:withEvent:用pointInside代替
通過注釋了解到hitTest方法是遞歸的調(diào)用pointInside方法。point是在接受控件坐標(biāo)系內(nèi)的禾怠。
底層的事件傳遞實(shí)現(xiàn)就是:
產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view->...->返回最合適的view
2.4 攔截事件傳遞
我們可以重寫hitTest方法返奉,來攔截系統(tǒng)的事件傳遞贝搁,讓指定的view處理事件。例如自定義view中芽偏,想讓view中的一個(gè)subview處理事件雷逆,就可以在自定義view中重寫該方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
return self;
}
return nil;
}
示例代碼我返回的是self,這里可以改成指定的subview污尉,或者遍歷subview中的一個(gè)膀哲。
3. 響應(yīng)鏈
在很多文章中都看到了這張圖,不清楚是不是官方十厢,但圖片中的邏輯是沒有問題的等太,ios控件間的擺放都是有層級(jí)關(guān)系的,這張圖表示的很清晰蛮放。響應(yīng)者對(duì)象就是繼承與UIResponder的子類們。
3.1 UIResponder的子類
UIResponder的子類有一下幾個(gè):
- AppDelegate
- UIApplication
- UIViewController
- UIView
p.s. UIWindow的父類是UIView
3.2 nextResponder
UIResponder的子類是通過nextResponder進(jìn)行連接的奠宜。
響應(yīng)鏈創(chuàng)建方式包颁,本人個(gè)人理解,應(yīng)該是鏈表的頭插法形式:
- AppDelegate作為整個(gè)鏈的根基压真,是第一個(gè)被創(chuàng)建出來的娩嚼,在main函數(shù)中被調(diào)用。它的nextResponder為nil滴肿。當(dāng)前鏈表的狀態(tài):AppDelegate->nil
- 系統(tǒng)提供給我們的UIApplication單例岳悟,響應(yīng)鏈變?yōu)椋?strong>UIApplication->AppDelegate->nil
- UIApplication會(huì)創(chuàng)建keyWindow,是UIWindow類型泼差,父類是UIView贵少,也是UIResponder的子類,所以響應(yīng)鏈變?yōu)椋?strong>keyWindow->UIApplication->AppDelegate->nil
- keyWindow中會(huì)設(shè)置一個(gè)rootViewController堆缘,是UIViewController類型滔灶,是UIResponder子類,rootViewController->keyWindow->UIApplication->AppDelegate->nil
- rootViewController中有view吼肥,我們?cè)陂_發(fā)中把自定義的view加載vc的view中录平,最終響應(yīng)鏈為:自定義view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil
這里只是簡(jiǎn)單舉個(gè)例子,其實(shí)項(xiàng)目中會(huì)有更復(fù)雜的層級(jí)關(guān)系缀皱。
3.3 官方文檔可以證明
很多人會(huì)問如何證明呢斗这,我們來看看官方文檔中的解釋:
Summary
Returns the next responder in the responder chain, or nil if there is no next responder.
返回響應(yīng)者鏈中的下一個(gè)響應(yīng)者,如沒有下一個(gè)響應(yīng)者返回nil啤斗。
Disussion
The UIResponder class does not store or set the next responder automatically, so this method returns nil by default. Subclasses must override this method and return an appropriate next responder. For example, UIView implements this method and returns the UIViewController object that manages it (if it has one) or its superview (if it doesn’t). UIViewController similarly implements the method and returns its view’s superview. UIWindow returns the application object. The shared UIApplication object normally returns nil, but it returns its app delegate if that object is a subclass of UIResponder and has not already been called to handle the event.
UIResponder類不會(huì)自動(dòng)存儲(chǔ)和設(shè)置下一個(gè)響應(yīng)者(next responder)表箭,這個(gè)方法默認(rèn)返回nil。子類必須復(fù)寫這個(gè)方法并且返回一個(gè)合適的下一個(gè)響應(yīng)者争占。例如燃逻,UIView實(shí)現(xiàn)這個(gè)方法序目,如果是被UIViewController對(duì)象管理的下一個(gè)響應(yīng)者就是UIViewController;如不哦不是被UIViewController對(duì)象管理的伯襟,下一個(gè)響應(yīng)者就是superview猿涨。UIViewController同樣實(shí)現(xiàn)這個(gè)方法,并且返回它自己view的superview姆怪。UIWindow返回application對(duì)象叛赚。shared UIApplication對(duì)象通常返回nil,但是如果該對(duì)象是一個(gè)UIRespnder的子類并且還沒有被調(diào)用去處理事件稽揭,它返回的是app的delegate俺附。
3.4 事件響應(yīng)鏈中的傳遞
通過上面例子中的響應(yīng)鏈自定義view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil的順序,逐層向后查找可做響應(yīng)的響應(yīng)者(UIResponder子類)溪掀。
如果多層有實(shí)現(xiàn)了UIResponder的相關(guān)方法事镣,例如touchesBegan,這多層都可以響應(yīng)揪胃。
舉個(gè)例子:
vc中init一個(gè)自定義的TestView璃哟,并且在vc和TestView中都實(shí)現(xiàn)了touchesBegan方法
vc部分代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = UIColor.lightGrayColor;
TestView *view1 = [[TestView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
view1.tag = 1;
view1.backgroundColor = [UIColor redColor];
[self.view addSubview:view1];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
[super touchesBegan:touches withEvent:event];
}
TestView部分代碼:
@implementation TestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
[super touchesBegan:touches withEvent:event];
}
運(yùn)行后的效果:
點(diǎn)擊紅色區(qū)域后查看控制臺(tái):
TestView和VC的touchesBegan方法都調(diào)用了。
注意:TestView中的touchesBegan要調(diào)用super touchesBegan喊递,如果不調(diào)用随闪,vc中無法打印。因?yàn)椴徽{(diào)用就不會(huì)繼續(xù)查找響應(yīng)鏈中后續(xù)的響應(yīng)者了骚勘。vc中touchesBegan中調(diào)用了super也是同理目的铐伴。
4. 簡(jiǎn)單總結(jié)
事件的傳遞和響應(yīng)的區(qū)別:
事件的傳遞是從上到下(父控件到子控件),事件的響應(yīng)是從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件)俏讹。
5. 應(yīng)用場(chǎng)景
參考這篇文章:iOS事件響應(yīng)鏈中hitTest的應(yīng)用示例
其中包括:
- 擴(kuò)大UIButton的響應(yīng)熱區(qū)
- 子view超出了父view的bounds響應(yīng)事件
- 使部分區(qū)域失去響應(yīng).
- 讓非scrollView區(qū)域響應(yīng)scrollView拖拽事件