事件傳遞機制

主要內容

  • 理論部分
  • 常見應用

理論部分

iOS中事件(UIEvent)主要是以下幾種,本文主要是分析觸控事件(UITouch)

  1. 觸控事件(UIEventTypeTouches):單點、多點觸控以及各種手勢操作
  2. 傳感器事件(UIEventTypeMotion):重力、加速度傳感器等
  3. 遠程控制事件(UIEventTypeRemoteControl):遠程遙控 iOS 設備多媒體播放等
  4. 按壓事件(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
  • 調用UIApplicationsendEvent:方法
  • 調用UIWindowsendEvent:方法
  • 目標視圖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;
}
image_1.png

圖片來源

關于事件響應鏈

當 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.superView
  • controller.view.nextResponder == controller
  • controller.nextResponder == controller.view.superView 如果存在一個控制器在另一個控制器中時
  • controller.nextResponder == window
  • window.nextResponder == Appliction

當一個 view 被 add 到 superView 上的時候,他的 nextResponder 屬性就會被指向它的 superView,當 controller 被初始化的時候,controller.view 的 nextResponder 會被指向所在的 controller,而 controller 的 nextResponder 會被指向 controller.view 的 superView,這樣整個 app 就通過 nextResponder 串成了一條鏈,也就是我們所說的響應鏈。

image_2.png

圖片來源

于是我們理解事件傳遞就很方便了,通過 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];
}

參考資料

  1. 深入淺出iOS事件機制
  2. iOS事件分發(fā)機制(一) hit-Testing
  3. iOS事件分發(fā)機制(二)The Responder Chain
  4. 事件傳遞響應鏈
  5. iOS觸摸事件的流動
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容