主要內容
- 理論部分
- 常見應用
理論部分
iOS中事件(UIEvent)主要是以下幾種,本文主要是分析觸控事件(UITouch)
- 觸控事件(UIEventTypeTouches):單點、多點觸控以及各種手勢操作
- 傳感器事件(UIEventTypeMotion):重力、加速度傳感器等
- 遠程控制事件(UIEventTypeRemoteControl):遠程遙控 iOS 設備多媒體播放等
- 按壓事件(UIEventTypePresses):3D Touch(iOS 9)
如果一個對象需要響應事件(UIEvent)那么就需要繼承 UIResponder。UIView、UIViewController、UIApplication 都繼承 UIResponder,所以它們都可以響應事件。
UITouch事件傳遞流程
- ...系統(tǒng)層面事件轉換...
- App主線程RunLoop被喚醒
- 主線程RunLoop觸發(fā)
__handleEventQueue方法 - 內部派發(fā)任務時,先通過
hitTest:withEvent:找到目標視圖hitTestView,封裝成UITouch(對象中包含hitTestView)添加到UIEvent中的allTouches中 - 調用
UIApplication的sendEvent:方法 - 調用
UIWindow的sendEvent:方法 - 目標視圖
hitTestView如果重載touchesBegan:,touchesEnded:,touchesMoved:,touchesCancelled:這些方法開始響應,如果沒有則會按照UIResponder響應鏈一直往上傳遞
PS:關于hitTest:withEvent:和sendEvent:的順序可以通過hook驗證
關于hitTestView(被點擊的View)
當點擊屏幕上的按鈕時,是怎么定位到按鈕呢?接下來我們來分析定位響應者,核心方法如下(UIView方法)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
每當手指接觸屏幕,UIApplication 接收到手指的事件之后,就會去調用 UIWindow 的 hitTest:withEvent:,看看當前點擊的點是不是在 window 內,如果是則繼續(xù)依次調用 subView 的 hitTest:withEvent: 方法,直到找到最后需要的 view。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if(self.isUserInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
{
return nil;
}
if([self pointInside:point withEvent:event])
{
//注意優(yōu)先遍歷上面的視圖,越晚添加越在上面
for(UIView *subView in self.subviews.reverseObjectEnumerator)
{
CGPoint convertPoint = [subView convertPoint:point fromView:self];
UIView *hitTestView = [subView hitTest:convertPoint withEvent:event];
if(hitTestView)
{
return hitTestView;
}
}
return self;
}
return nil;
}

關于事件響應鏈
當 UIApplication 在 UIWindow 中找到 hitTestView 時,通過 sendEvent: 把事件(UIEvent)發(fā)送給UIWindow,UIWindow 也通過 sendEvent: 向 hitTestView 發(fā)送消息。這個消息是否可以響應就需要依賴 UIResponder。
//與本文相關的主要內容
@interface UIResponder : NSObject
@property(nonatomic, readonly) UIResponder *nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;
@property(nonatomic, readonly) BOOL canResignFirstResponder;
- (BOOL)becomeFirstResponder;
- (BOOL)resignFirstResponder;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
@end
UIResponder 中的 nextResponder 是事件傳遞的關鍵。nextResponder 構成傳遞鏈有以下關系:
view.nextResponder == view.superViewcontroller.view.nextResponder == controller-
controller.nextResponder == controller.view.superView如果存在一個控制器在另一個控制器中時 controller.nextResponder == windowwindow.nextResponder == Appliction
當一個 view 被 add 到 superView 上的時候,他的 nextResponder 屬性就會被指向它的 superView,當 controller 被初始化的時候,controller.view 的 nextResponder 會被指向所在的 controller,而 controller 的 nextResponder 會被指向 controller.view 的 superView,這樣整個 app 就通過 nextResponder 串成了一條鏈,也就是我們所說的響應鏈。

于是我們理解事件傳遞就很方便了,通過 hitTest 找到需要響應的 hitTestView,然后向其發(fā)送消息,如果 hitTestView 可以響應手勢、Button 或實現(xiàn)了 touchesBegan、touchesEnded 等方法,那么事件找到了組織了,否則通過 nextResponder 鏈一直找下去。
常見應用
理解了事件傳遞機制,我們可以靈活的運用尋找 hitTestView 和事件傳遞。
1、 擴大響應范圍
我們經常遇到這樣的情形,就是我們希望擴大按鈕的點擊區(qū)域。這時我們可以通過 hitTest 來進行控制。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if(self.isUserInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
{
return nil;
}
//上下左右擴大20px
CGRect newRect = CGRectInset(self.bounds, -20, -20);
if(CGRectContainsPoint(newRect, point))
{
for(UIView *subView in self.subviews.reverseObjectEnumerator)
{
CGPoint convertPoint = [subView convertPoint:point fromView:self];
UIView *hitTestView = [subView hitTest:convertPoint withEvent:event];
if(hitTestView)
{
return hitTestView;
}
}
return self;
}
return nil;
}
2、事件轉發(fā)
有時我們需要實現(xiàn),彈個窗顯示一些內容,內容外部需要一個半透明的擋板,當點擊擋板時,關閉彈窗。我之前會為擋板添加一個手勢來關閉,如果了解了事件轉發(fā)機制,我們只需要重載一下 touchesBegan: withEvent: 即可。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
[self close];
}