史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-實(shí)踐篇

前言

之前我已經(jīng)通過《史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇》比較詳細(xì)的介紹過了事件的響應(yīng)和傳遞的一些原理。如果說上篇是原理性文章,那么本篇文章更偏重于實(shí)踐。本篇文章主要介紹如何利用事件處理的這些機(jī)制來處理公司開發(fā)中一些比較棘手的需求。例如,點(diǎn)擊的是A視圖,卻要讓B視圖處理事件;點(diǎn)擊子視圖,卻要讓父視圖處理事件等等。今天,我整理了下之前的雜記,羅列出了一些開發(fā)中可能遇到的情景和應(yīng)對措施!當(dāng)然,這要求我們對事件的傳遞和響應(yīng)機(jī)制非常了解。如果對此不太了解,請閱讀筆者的《史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇》

視圖層次

如上圖,視圖層次結(jié)構(gòu):白色->紅色->綠色。紅色的view是綠色view的父視圖,白色的view又是紅色view的父視圖。如下要求:

需求情景一

當(dāng)子控件和父控件重疊時,點(diǎn)擊子控件,子控件響應(yīng)事件。也就是說,點(diǎn)擊綠色的view和紅色的view的重疊部分,只有綠色的view響應(yīng)事件。
分析:其實(shí)這算不上一個需求,因?yàn)橄到y(tǒng)默認(rèn)就是這樣處理的。重寫綠色view的 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法,默認(rèn)就是綠色的view響應(yīng)事件,但是僅限于重疊部分,點(diǎn)擊綠色view上的非重疊部分,綠色和紅色view都不會響應(yīng)。
原因在于,系統(tǒng)從window向下尋找最合適的view時候,遍歷到紅色的view時候,發(fā)現(xiàn)點(diǎn)不在紅色的view上,那么默認(rèn)控制器的view就是最合適的view。即控制器的view響應(yīng)了事件。

#import "GreenView.h"

@implementation GreenView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"greenView %s",__func__);
}
@end

打印結(jié)果

2016-02-27 20:49:46.234 事件處理和響應(yīng)[905:44466] greenView -[GreenView touchesBegan:withEvent:]
2016-02-27 20:49:48.051 事件處理和響應(yīng)[905:44466] greenView -[GreenView touchesBegan:withEvent:]

需求情景二

當(dāng)子控件和父控件部分重疊時,點(diǎn)擊父子控件重疊部分,只有父控件響應(yīng)事件。也就是說,點(diǎn)擊綠色的view和紅色的view的重疊部分,只有紅色的view響應(yīng)事件。
分析:點(diǎn)擊子控件,卻要讓父控件響應(yīng)事件,說明子控件本身不是最合適的view,父控件才是最合適的view,因?yàn)閔itTest:withEvent:方法的作用就是控件接收到事件后,判斷自己是否能處理事件,判斷點(diǎn)在不在自己的坐標(biāo)系上,然后返回最合適的view。所以,我們可以在hitTest:withEvent:方法里面強(qiáng)制返回父控件為最合適的view,也就是返回紅色的view。
注意,不能夠重寫父控件的hitTest:withEvent:方法,也就是不能夠重寫紅色的view的hitTest:withEvent:方法。原因在于,如果重寫父控件的hitTest:withEvent:方法,并在該方法中返回父控件本身,會導(dǎo)致點(diǎn)擊父控件的父控件時,也是父控件為最合適的view。反應(yīng)在上面的例子上就是,點(diǎn)擊白色的地方,也是紅色的view響應(yīng)事件。

#import "GreenView.h"

@implementation GreenView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return [self superview]; // return nil; // 此處返回nil也可以。返回nil就相當(dāng)于當(dāng)前的view不是最合適的view
}

@end

打印結(jié)果:

2016-02-27 20:52:09.083 事件處理和響應(yīng)[921:46215] redView -[RedView touchesBegan:withEvent:]
2016-02-27 20:52:10.154 事件處理和響應(yīng)[921:46215] redView -[RedView touchesBegan:withEvent:]

需求情景三

無論點(diǎn)擊什么地方,父控件響應(yīng)事件。也就是說,點(diǎn)擊白色的view、紅色的view或者綠色的view上的任一點(diǎn)(屏幕上任一點(diǎn)),都只是紅色的view響應(yīng)事件。
分析:點(diǎn)擊屏幕上任意點(diǎn),都是紅色的view響應(yīng)事件,根據(jù)視圖層次結(jié)構(gòu),我們只需要重寫紅色View的hit:test:方法,并在此方法中返回紅色的view即可。

@implementation RedView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"RedView %s",__func__);
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return self;
}
@end

打印結(jié)果

2016-02-27 20:58:29.883 CharactersRange[4843:853272] RedView -[RedView touchesBegan:withEvent:]
2016-02-27 20:58:30.653 CharactersRange[4843:853272] RedView -[RedView touchesBegan:withEvent:]
2016-02-27 20:58:31.479 CharactersRange[4843:853272] RedView -[RedView touchesBegan:withEvent:]
2016-02-27 20:58:32.199 CharactersRange[4843:853272] RedView -[RedView touchesBegan:withEvent:]

需求情景四

當(dāng)子控件和父控件部分重疊時,點(diǎn)擊非重疊部分,父控件響應(yīng)事件。也就是說,點(diǎn)擊綠色的view的上半部分(即不重疊的部分),紅色的view響應(yīng)事件。
分析:我們知道,系統(tǒng)尋找能夠處理事件的最合適的view有兩個標(biāo)準(zhǔn)。第一,這個view能接收事件,第二,這個點(diǎn)在自己身上。這兩個條件缺一不可。此處我們點(diǎn)擊的點(diǎn)是綠色的view的上部分,這個點(diǎn)沒有在紅色的view的坐標(biāo)系上。 也就是當(dāng)事件傳遞給紅色的view時,紅色的view雖然能夠接收這個事件,但是點(diǎn)不在紅色的view的坐標(biāo)系上,所以紅色的view不是最合適的view,這個事件就不會交給紅色的view處理。紅色的view作為父控件都沒有接收到這個事件,當(dāng)然他的子控件綠色的view也肯定接收不到這個觸摸事件。
我起初嘗試重寫紅色的view的hitTest:withEvent:方法,返回紅色的view為最合適的view。雖然可以解決問題,但是點(diǎn)擊其他任何地方,也是紅色的view響應(yīng)事件。這個結(jié)果不是我想要的,我只希望點(diǎn)擊紅色和綠色的部分,紅色能夠響應(yīng)事件,至于點(diǎn)擊其他部分,我不需要紅色響應(yīng)事件,那么該怎么做呢?
隨后,我又重寫了綠色的view的hitTest:withEvent:方法,返回紅色的view。但是,點(diǎn)擊非重疊區(qū),沒響應(yīng)。
最后的解決方法是這樣的:

#import "RedView.h"

@implementation RedView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"redView %s",__func__);
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 判斷被點(diǎn)擊的點(diǎn)在不在自己身上
    // 把被點(diǎn)擊的點(diǎn)轉(zhuǎn)換成子控件坐標(biāo)系的點(diǎn)
    CGPoint greenViewPoint = [self convertPoint:point toView:self.subviews[0]];
 
    if ([self pointInside:point withEvent:event] || [self.subviews[0] pointInside:greenViewPoint withEvent:event]) { // 觸摸點(diǎn)在自己身上或者在子控件身上都返回自己作為最合適的view
        return self;
    }
    return  [super hitTest:point withEvent:event];
}
@end

需求情景五

當(dāng)子控件和父控件部分重疊時,點(diǎn)擊子控件,父控件和子控件都響應(yīng)事件。也就是說,點(diǎn)擊綠色的view,不但綠色的view本身響應(yīng)事件,紅色的view也響應(yīng)事件。

分析:事件的響應(yīng)是順著響應(yīng)者鏈條向上傳遞的,即從子控件傳遞給父控件,touch方法默認(rèn)不處理事件,而是把事件順著響應(yīng)者鏈條傳遞給上一個響應(yīng)者。這樣我們就可以依托這個原理,讓一個事件多個控件響應(yīng)。

#import "GreenView.h"

@implementation GreenView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"greenView %s",__func__); // 重寫touch方法,自己可以響應(yīng)事件
    [super touchesBegan:touches withEvent:event]; // 在調(diào)用系統(tǒng)默認(rèn)的方法,又把事件順著響應(yīng)者鏈條拋給上一個響應(yīng)者。這就做到了一個事件,多個控件響應(yīng)。
}
@end

打印結(jié)果:

這是一次點(diǎn)擊:
2016-02-27 21:23:37.503 事件處理和響應(yīng)[1020:60725] greenView -[GreenView touchesBegan:withEvent:]
2016-02-27 21:23:37.504 事件處理和響應(yīng)[1020:60725] redView -[RedView touchesBegan:withEvent:]

文/VV木公子(簡書作者)PS:如非特別說明,所有文章均為原創(chuàng)作品,著作權(quán)歸作者所有,轉(zhuǎn)載轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),并注明出處,所有打賞均歸本人所有!
如果您是iOS開發(fā)者,或者對本篇文章感興趣,請關(guān)注本人,后續(xù)會更新更多相關(guān)文章!敬請期待!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容