iOS--事件傳遞/響應(yīng)者鏈

應(yīng)用程序使用響應(yīng)者對(duì)象接收和處理事件。響應(yīng)者對(duì)象是UIResponder類的任何實(shí)例,常見(jiàn)的子類包括UIView、UIViewController和UIApplication。響應(yīng)者接收原始事件數(shù)據(jù),必須處理該事件或?qū)⑵滢D(zhuǎn)發(fā)給另一個(gè)響應(yīng)程序?qū)ο?。?dāng)應(yīng)用程序接收到事件時(shí),UIKit會(huì)自動(dòng)將該事件定向到最合適的響應(yīng)程序?qū)ο螅吹谝豁憫?yīng)者。

在iOS程序中響應(yīng)者對(duì)象的擺放是有前后關(guān)系的,多個(gè)響應(yīng)者對(duì)象有序的連接起來(lái)的鏈條就叫“響應(yīng)者鏈”。

創(chuàng)建一個(gè)UIView的子類ChainView,重寫(xiě)touchesBegan方法:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ --- touchesBegan withEvent ---", self.name);
    UIResponder * next = [self nextResponder];
    NSMutableString * prefix = @"".mutableCopy;
    while (next != nil) {
        NSLog(@"%@%@", prefix, [next class]);
        [prefix appendString: @"--"];
        next = [next nextResponder];
    }
}

創(chuàng)建一個(gè)ChainView實(shí)例添加到ViewController.view,點(diǎn)擊log輸出:

2020-09-24 11:08:56.494864+0800 001--ClientDemo[83858:1377593] greenView --- touchesBegan withEvent ---
ClientDemo[83858:1377593] UIView
ClientDemo[83858:1377593] --ViewController
ClientDemo[83858:1377593] ----UIWindow
ClientDemo[83858:1377593] ------UIApplication
ClientDemo[83858:1377593] --------AppDelegate

選擇不同版本iOS有不同輸出,在iOS12之前是上面輸出,iOS12及以后輸出如下

2020-09-24 11:11:41.146777+0800 001--ClientDemo[84116:1402680] greenView --- touchesBegan withEvent ---
ClientDemo[84116:1402680] UIView
ClientDemo[84116:1402680] --ViewController
ClientDemo[84116:1402680] ----UIDropShadowView
ClientDemo[84116:1402680] ------UITransitionView
ClientDemo[84116:1402680] --------UIWindow
ClientDemo[84116:1402680] ----------UIWindowScene
ClientDemo[84116:1402680] ------------UIApplication
ClientDemo[84116:1402680] --------------AppDelegate

這里及以下都以iOS11上為例。上面打印的是本例中事件響應(yīng)傳遞關(guān)系也即事件響應(yīng)者鏈,響應(yīng)者鏈?zhǔn)鞘录鬟f鏈的逆序,則事件傳遞鏈如下圖:

事件傳遞鏈

UIView 的 controller 是直接管理它的 UIViewController (也就是 VC.view.nextResponder = VC ),如果當(dāng)前 View 不是 ViewController 直接管理的 View,則 nextResponder 是它的 superView( view.nextResponder = view.superView )。
UIViewController 的 nextResponder 是它直接管理的 View 的 superView( VC.nextResponder = VC.view.superView )。
UIWindow 的 nextResponder 是 UIApplication 。
UIApplication 的 nextResponder 是 AppDelegate。

有了響應(yīng)鏈,并且找到了第一個(gè)響應(yīng)事件的對(duì)象,接下來(lái)就是把事件發(fā)送給這個(gè)響應(yīng)者了。 UIApplication中有個(gè)sendEvent:的方法,在UIWindow中同樣也可以發(fā)現(xiàn)一個(gè)同樣的方法。UIApplication是通過(guò)這個(gè)方法把事件發(fā)送給UIWindow,然后UIWindow通過(guò)同樣的API,把事件發(fā)送給hit-testview。

響應(yīng)鏈工作步驟

  1. 事件產(chǎn)生
  2. 事件順著傳遞鏈 AppDelegate---> UIApplication --->UIWindow ---> 查找第一響應(yīng)者,查找過(guò)程主要用到兩個(gè)函數(shù):
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  1. 找到最合適響應(yīng)者視圖后,視圖會(huì)調(diào)用自己的touches方法處理事件。如果自身沒(méi)有做處理,UIKit那么會(huì)逆著事件傳遞鏈,向上查找,直到找到能夠響應(yīng)這個(gè)事件的視圖。當(dāng)將事件傳遞給UIApplication對(duì)象時(shí),如果該對(duì)象是UIResponder實(shí)例,而不屬于響應(yīng)程序鏈的一部分,則可能會(huì)將事件傳遞給應(yīng)用程序委托AppDelegate。最終如果沒(méi)有對(duì)象響應(yīng)該事件,該事件會(huì)被丟棄(不是Crash?。?。

示例

左側(cè)是實(shí)現(xiàn)效果,其中設(shè)置了cyanView.userInteractionEnabled = NO,右側(cè)是當(dāng)前程序的響應(yīng)者鏈(同級(jí)視圖左邊線與右邊添加到父視圖上)。


1600846386263.jpg

修改上面提到的的ChainView實(shí)現(xiàn):

@interface ChainView : UIView
@property(nonatomic, copy) NSString *name;
@end

@implementation ChainView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ --- touchesBegan withEvent ---", self.name);
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@ --- hitTest withEvent", self.name);
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"%@ --- hitTest withEvent --- hitTestView:%@", self.name, ((ChainView *)view).name);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"%@ --- pointInside withEvent --- isInside:%d", self.name, isInside);
    return isInside;
}
@end

點(diǎn)擊blueView和cyanView的重疊處,log輸出:

2020-09-23 16:48:39.439271+0800 TT[72680:1155021] redView --- hitTest withEvent
2020-09-23 16:48:39.439426+0800 TT[72680:1155021] redView --- pointInside withEvent
2020-09-23 16:48:39.439538+0800 TT[72680:1155021] redView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.439644+0800 TT[72680:1155021] yllowView --- hitTest withEvent
2020-09-23 16:48:39.439933+0800 TT[72680:1155021] yllowView --- pointInside withEvent
2020-09-23 16:48:39.440452+0800 TT[72680:1155021] yllowView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.440935+0800 TT[72680:1155021] purpleView --- hitTest withEvent
2020-09-23 16:48:39.441447+0800 TT[72680:1155021] purpleView --- pointInside withEvent
2020-09-23 16:48:39.442443+0800 TT[72680:1155021] purpleView --- pointInside withEvent --- isInside:0
2020-09-23 16:48:39.442865+0800 TT[72680:1155021] purpleView --- hitTest withEvent --- hitTestView:(null)
2020-09-23 16:48:39.443304+0800 TT[72680:1155021] orangeView --- hitTest withEvent
2020-09-23 16:48:39.443728+0800 TT[72680:1155021] orangeView --- pointInside withEvent
2020-09-23 16:48:39.444398+0800 TT[72680:1155021] orangeView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.444861+0800 TT[72680:1155021] cyanView --- hitTest withEvent
2020-09-23 16:48:39.445325+0800 TT[72680:1155021] cyanView --- hitTest withEvent --- hitTestView:(null)
2020-09-23 16:48:39.445743+0800 TT[72680:1155021] blueView --- hitTest withEvent
2020-09-23 16:48:39.446166+0800 TT[72680:1155021] blueView --- pointInside withEvent
2020-09-23 16:48:39.446590+0800 TT[72680:1155021] blueView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.446985+0800 TT[72680:1155021] blueView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.447281+0800 TT[72680:1155021] orangeView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.447678+0800 TT[72680:1155021] yllowView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.448096+0800 TT[72680:1155021] redView --- hitTest withEvent --- hitTestView:blueView
  1. 首先是redView的hitTest被調(diào)用,hitTest里通過(guò)pointInside判斷事件在redView相應(yīng)范圍內(nèi),向下判斷redView的子視圖;
  2. 子視圖的判斷是逆序的,所以先判斷yellowView,事件在yellowView響應(yīng)范圍,繼續(xù)判斷yellowView的子視圖;
  3. 依然是因?yàn)樽右晥D的判斷是逆序的,所以先判斷purpleView,沒(méi)有在purpleView響應(yīng)范圍;判斷orangeView通過(guò),繼續(xù)向下判斷orangeView的子視圖;
  4. 在判斷cyanView的時(shí)候,因?yàn)樵O(shè)置了cyanView.userInteractionEnabled = NO,所以cyanView不能響應(yīng)事件;
  5. 最后判斷blueView是第一響應(yīng)者。逆著響應(yīng)者鏈通知前面的響應(yīng)者 blueView是第一響應(yīng)者,blueView的touches事件調(diào)用。

事件攔截

改變第一響應(yīng)者

image.png

在需要攔截的 view 中重寫(xiě) hitTest 方法改變第一響應(yīng)者。
比如上面示例中,讓事件在yellowView中斷,由yellowView響應(yīng):
在ChainView中重新實(shí)現(xiàn)hitTest:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 
    0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        if([self.name isEqualToString:@"yellowView"]){
             return self;
         }
    }
    return nil;
}

限制第一響應(yīng)者范圍

下圖中的按鈕,只有中間圓形部分可以點(diǎn)擊,四個(gè)角上不響應(yīng)事件:


CustomButton

創(chuàng)建一個(gè)繼承自UIButton的CustomButton,實(shí)現(xiàn):

@implementation CustomButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    // 判斷觸摸位置是否在當(dāng)前視圖內(nèi)
    if ([self pointInside:point withEvent:event]) {
        //逆序遍歷當(dāng)前對(duì)象的子視圖
        __block UIView *hitView = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //坐標(biāo)轉(zhuǎn)換系 使坐標(biāo)基于子視圖
            CGPoint convertedPoint = [self convertPoint:point toView:obj];
            //調(diào)用子視圖的 hitTest 方法
            hitView = [obj hitTest:convertedPoint withEvent:event];
            // 如果子視圖返回一個(gè)view 遍歷終止
            if (hitView) {
                *stop = YES;
            }
        }];
        //如果子視圖返回一個(gè)view 返回這個(gè)view
        if(hitView) {
            return hitView;
        }
        
        //所有子視圖都返回 nil, 則返回自身.
        return self;
    }
    return nil;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;
    
    CGFloat x2 = self.frame.size.width/2;
    CGFloat y2 = self.frame.size.height/2;
    
    double dist = sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
    
    //在以當(dāng)前控件中心為圓心,直徑為控件寬度的圓內(nèi)
    if(dist <= self.frame.size.width/2){
        return YES;
    }
    else{
        return NO;
    }
    
//    return [super pointInside:point withEvent:event];
}

具體使用和UIButton一樣,有興趣可以自己測(cè)試一下效果。

事件轉(zhuǎn)發(fā)

有時(shí)候還需要將事件轉(zhuǎn)發(fā)出去。讓本不能響應(yīng)事件的 view 響應(yīng)事件,最常用的場(chǎng)景就是讓子視圖超出父視圖的部分也能響應(yīng)事件


image.png

cyanView的右下區(qū)域超出了父視圖 orangeView 的區(qū)域,如果不作處理,那么點(diǎn)擊超出的區(qū)域是無(wú)法響應(yīng)事件的,因?yàn)槌鰠^(qū)域的坐標(biāo)不在orangeView的范圍內(nèi),當(dāng)執(zhí)行到orangeView的 pointInside 的時(shí)候就會(huì)返回 NO。
想要讓超出區(qū)域響應(yīng)事件,就需要重寫(xiě)父視圖的 pointInside 或 hitTest 方法讓 pointInside 返回 YES 或 讓hitTest 直接返回cyanView
重寫(xiě)hitTest

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@ --- hitTest withEvent", self.name);
    
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    
    // 觸摸點(diǎn)在視圖范圍內(nèi) 則交由父類處理
    if ([self pointInside:point withEvent:event]) {
        return [super hitTest:point withEvent:event];
    }

    // 如果觸摸點(diǎn)不在范圍內(nèi) 而在子視圖范圍內(nèi)依舊返回子視圖
    NSArray<UIView *> * superViews = self.subviews;
    // 倒序 從最上面的一個(gè)視圖開(kāi)始查找
    for (NSUInteger i = superViews.count; i > 0; i--) {
        UIView * subview = superViews[i - 1];
        // 轉(zhuǎn)換坐標(biāo)系 使坐標(biāo)基于子視圖
        CGPoint newPoint = [self convertPoint:point toView:subview];
        // 得到子視圖 hitTest 方法返回的值
        UIView * view = [subview hitTest:newPoint withEvent:event];
        // 如果子視圖返回一個(gè)view 就直接返回 不在繼續(xù)遍歷
        if (view) {
            return view;
        }
    }
    return nil;

重寫(xiě) pointInside 方法原理相同, 重點(diǎn)注意轉(zhuǎn)換坐標(biāo)系,就算他們不是一條響應(yīng)鏈上,也可以通過(guò)重寫(xiě) hitTest 方法轉(zhuǎn)發(fā)事件。

參考: http://www.itdecent.cn/p/69c578165054

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

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