以往每次在處理到攔截事件或者傳遞事件的時候,會想到響應(yīng)者鏈,通過重寫hit-testing方法去修改,但是真正寫的時候卻不知道該怎么下筆,總是要去百度,還是對這塊的知識不太熟悉。在看了幾篇博客和官方文檔后,依據(jù)自己的理解,將這一個知識點記錄下來,加強理解和記憶,以便在今后的開發(fā)過程中可以更高效快速的完成任務(wù)。
iOS的事件分發(fā)機制即尋找最佳響應(yīng)視圖并回調(diào)該視圖對事件的處理,該過程分為兩個步驟:
- 尋找最佳響應(yīng)者
系統(tǒng)在捕獲到用戶事件后,會將其封裝成UIEvent對象發(fā)送給當前的UIApplication,然后會調(diào)用window和其子視圖的hit-testing方法找到最佳響應(yīng)視圖,通過重寫hit-testing方法決定返回哪個視圖。
尋找最佳響應(yīng)者的流程圖:

123.jpg
hit-testing實現(xiàn)邏輯:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判斷view能否接收事件
if (self.userInteractionEnabled == NO ||
self.hidden == YES ||
self.alpha <= 0.01) {
return nil;
}
// 2.判斷觸摸點是否在view內(nèi)
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 3.遞歸調(diào)用view的所有子view,尋找最佳響應(yīng)視圖
for (UIView *subView in self.subviews.reverseObjectEnumerator) {
//轉(zhuǎn)換到子視圖坐標系上
CGPoint childPoint = [self convertPoint:point toView:subView];
UIView *fitView = [subView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
return self;
}
- 處理事件
當找到最佳響應(yīng)視圖后,同時也得到了一條響應(yīng)者鏈,默認情況下會由該視圖去處理事件,但是通過nextResponder屬性,在同一響應(yīng)者鏈上的其他響應(yīng)者,也是可以處理該事件的。而且也可以重寫nextResponder屬性,來改變下一個響應(yīng)者。
許多 UIKit 類已經(jīng)覆蓋了這個屬性并返回了特定的對象,包括
| 響應(yīng)鏈節(jié)點 | 表現(xiàn) |
|---|---|
| UIView | 如果view是viewcontroller的根view,那么下一個響應(yīng)者是viewcontroller,否則是super view |
| UIViewcontroller | 如果viewcontroller的view是window的根view,那么下一個響應(yīng)者是window;如果viewcontroller是另一個viewcontroller模態(tài)推出的,那么下一個響應(yīng)者是另一個viewcontroller;如果viewcontroller的view被add到另一個viewcontroller的根view上,那么下一個響應(yīng)者是另一個viewcontroller的根view |
| UIWindow | UIWindow的下一個響應(yīng)者是UIApplication |
| UIApplication | 通常UIApplication是響應(yīng)者鏈的頂端(如果app delegate也繼承了UIResponder,事件還會繼續(xù)傳給app delegate) |
搞清楚了這個流程后,就可以針對一些場景提出合適的解決方案。
- 改變視圖的點擊范圍
// 重寫需要修改點擊范圍的view的pointInside方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 上下左右各加100
CGRect rect = CGRectInset(self.bounds, -100, -100);
return CGRectContainsPoint(rect, point);
}
- 子視圖不響應(yīng)事件
// 重寫該view的hit-testing方法,這樣該視圖的父視圖會響應(yīng)事件,同時也不會影響同級別其他視圖
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return nil;
}
- 父視圖和子視圖同時響應(yīng)事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ %@", self, NSStringFromSelector(_cmd));
// [super touchesBegan:touches withEvent:event];
[self.nextResponder touchesBegan:touches withEvent:event];
}
4.擴展UIResponder類,讓響應(yīng)鏈上的視圖都有機會處理事件
@interface UIResponder (Add)
- (void)routerEvent:(id)info;
@end
@implementation UIResponder (Add)
- (void)routerEvent:(id)info {
[self.nextResponder routerEvent:info];
}
@end
#import "GreenView.h"
@implementation GreenView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ %@", self, NSStringFromSelector(_cmd));
[self routerEvent:[NSString stringWithFormat:@"%@ 傳遞事件出去", self]];
}
...
@end
#import "ViewController.h"
#import "UIResponder+Add.h"
@implementation ViewController
- (void)routerEvent:(NSString *)info {
NSLog(@"info = %@", info);
[self.nextResponder routerEvent:info];
}
...
@end
<GreenView: 0x7fee2da0a280; frame = (10 10; 200 100); autoresize = RM+BM; layer = <CALayer: 0x600000403d60>> touchesBegan:withEvent:
info = <GreenView: 0x7fee2da0a280; frame = (10 10; 200 100); autoresize = RM+BM; layer = <CALayer: 0x600000403d60>> 傳遞事件出去
參考文檔: