一文搞懂響應(yīng)者鏈

以往每次在處理到攔截事件或者傳遞事件的時候,會想到響應(yīng)者鏈,通過重寫hit-testing方法去修改,但是真正寫的時候卻不知道該怎么下筆,總是要去百度,還是對這塊的知識不太熟悉。在看了幾篇博客和官方文檔后,依據(jù)自己的理解,將這一個知識點記錄下來,加強理解和記憶,以便在今后的開發(fā)過程中可以更高效快速的完成任務(wù)。

iOS的事件分發(fā)機制即尋找最佳響應(yīng)視圖并回調(diào)該視圖對事件的處理,該過程分為兩個步驟:

  1. 尋找最佳響應(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;
}
  1. 處理事件

當找到最佳響應(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)

搞清楚了這個流程后,就可以針對一些場景提出合適的解決方案。

  1. 改變視圖的點擊范圍
// 重寫需要修改點擊范圍的view的pointInside方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 上下左右各加100
    CGRect rect = CGRectInset(self.bounds, -100, -100);
    return CGRectContainsPoint(rect, point);
}
  1. 子視圖不響應(yīng)事件
// 重寫該view的hit-testing方法,這樣該視圖的父視圖會響應(yīng)事件,同時也不會影響同級別其他視圖
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return nil;
}
  1. 父視圖和子視圖同時響應(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>> 傳遞事件出去

參考文檔:

1.Using Responders and the Responder Chain to Handle Events

2.iOS 事件分發(fā)機制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容