iOS響應(yīng)者鏈、事件的傳遞

1、響應(yīng)鏈的傳遞

Responder一點(diǎn)也不神秘————iOS用戶響應(yīng)者鏈完全剖析(建議全看)
看完上面一篇應(yīng)該能完全熟悉了響應(yīng)鏈的傳遞,自己可以打印一下響應(yīng)鏈看看,代碼如下:

- (IBAction)click:(id)sender {
    UIResponder *res = sender;
    
    while (res) {
        NSLog(@"*************************************\n%@",res);
        res = [res nextResponder];
    }
}

2、Hit-Test 機(jī)制

當(dāng)用戶觸摸(Touch)屏幕進(jìn)行交互時(shí),系統(tǒng)首先要找到響應(yīng)者(Responder)。系統(tǒng)檢測(cè)到手指觸摸(Touch)操作時(shí),將Touch 以UIEvent的方式加入U(xiǎn)IApplication事件隊(duì)列中。UIApplication從事件隊(duì)列中取出最新的觸摸事件進(jìn)行分發(fā)傳遞到UIWindow進(jìn)行處理。UIWindow 會(huì)通過hitTest:withEvent:方法尋找觸碰點(diǎn)所在的視圖,這個(gè)過程稱之為hit-test view。
hitTest 的順序如下

UIApplication -> UIWindow -> Root View -> ··· -> subview

在頂級(jí)視圖(Root View)上調(diào)用pointInside:withEvent:方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi);

如果返回NO,那么hitTest:withEvent:返回nil;

如果返回YES,那么它會(huì)向當(dāng)前視圖的所有子視圖發(fā)送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢。

如果有subview的hitTest:withEvent:返回非空對(duì)象則A返回此對(duì)象,處理結(jié)束(注意這個(gè)過程,子視圖也是根據(jù)pointInside:withEvent:的返回值來確定是返回空還是當(dāng)前子視圖對(duì)象的。并且這個(gè)過程中如果子視圖的hidden=YES、userInteractionEnabled=NO或者alpha小于0.1都會(huì)并忽略);

如果所有subview遍歷結(jié)束仍然沒有返回非空對(duì)象,則hitTest:withEvent:返回self;

系統(tǒng)就是這樣通過hit test找到觸碰到的視圖(Initial View)進(jìn)行響應(yīng)。

如果還不清楚Hit-Test 機(jī)制,看更加清晰的Hit-Test 機(jī)制(建議還不清楚的看)

Paste_Image.png

3、手勢(shì)的原理及與touches系列的關(guān)系,具體的可以看iOS觸摸事件傳遞響應(yīng)之被忽視的手勢(shì)識(shí)別器工作原理(建議不看也沒關(guān)系,結(jié)論在下面了。)

簡(jiǎn)而言之,就是下面這幅圖了。觸摸事件會(huì)優(yōu)先分發(fā)給附在view的手勢(shì),在這段延遲的期間,如果手勢(shì)被識(shí)別,那么view的touches系列將被立刻取消,如果沒有被識(shí)別,那么會(huì)繼續(xù)我們所熟知的touches系列流程。


Paste_Image.png

4、實(shí)際開發(fā)中常見的相關(guān)問題

在實(shí)際開發(fā)中,經(jīng)常會(huì)遇到視圖沒有響應(yīng)的情況,特別是新手會(huì)經(jīng)常搞不清楚狀況。

一下是視圖沒有響應(yīng)的幾個(gè)情況:

1.userInteractionEnabled=NO;

2.hidden=YES;

3.alpha=0~0.01;

4.沒有實(shí)現(xiàn)touchesBegan:withEvent:方法,直接執(zhí)行touchesMove:withEvent:等方法;

5.目標(biāo)視圖點(diǎn)擊區(qū)域不在父視圖的Frame上 (superView背景色為clear Color的時(shí)候經(jīng)常會(huì)忽略這個(gè)問題)。

5、手勢(shì)代理

ios手勢(shì)識(shí)別代理,看這個(gè)基本上就夠了。引用文章中的一段話,如下:

  • 當(dāng)時(shí)做項(xiàng)目時(shí)這個(gè)主控制器就是RootViewController,雖然用的是ScrollView但也沒考慮到導(dǎo)航欄的手勢(shì)返回的問題 ,現(xiàn)在做小區(qū)寶3.0的閃購(gòu)訂單,用之前的就有問題了。導(dǎo)航欄的返回手勢(shì)用不了,根據(jù)響應(yīng)者鏈和響應(yīng)事件,手勢(shì)被ScrollView識(shí)別了,就到不了導(dǎo)航的手勢(shì)識(shí)別,所以導(dǎo)致無法手勢(shì)返回。

我也曾經(jīng)處理過這樣的問題,不過我那時(shí)候是帶有QQ的側(cè)滑功能,主控制器用的View是ScrollView,導(dǎo)致不能側(cè)滑。但是處理的方法都是一樣的,自定義的ScrollView的代碼重寫gestureRecognizerShouldBegin方法如下,我是手勢(shì)方向向右并且x軸起點(diǎn)小于60px的,讓ScrollView的手勢(shì)失效。這樣就不會(huì)截獲對(duì)應(yīng)的事件了。但是其實(shí)看完上面,還有更簡(jiǎn)單的方法,就是讓ScrollView的手勢(shì)共存,但是這樣可能會(huì)帶來一些其它的問題。shouldRecognizeSimultaneouslyWithGestureRecognizer設(shè)置為true,不過應(yīng)該要判斷手勢(shì)為UIScreenEdgePanGestureRecognizer時(shí)才return true,這樣就可以了。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
    CGPoint location = [gestureRecognizer locationInView:self];
    
    NSLog(@"velocity.x:%f----location.x:%d",velocity.x,(int)location.x%(int)[UIScreen mainScreen].bounds.size.width);
    if (velocity.x > 0.0f&&(int)location.x%(int)[UIScreen mainScreen].bounds.size.width<60) {
        return NO;
    }
    return YES;
} 

案例分析

案例一

下面這種做法,除非你很熟悉,否則不要這么干。因?yàn)?[super touchesBegan:touches withEvent:event];會(huì)執(zhí)行原來默認(rèn)的操作,如果按鈕本來就沒有添加對(duì)應(yīng)的事件。那么[[self nextResponder] touchesBegan:touches withEvent:event];和[super touchesBegan:touches withEvent:event];將會(huì)向下一響應(yīng)者發(fā)送兩次事件。

-(void)touchesBegan:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event{
    if (self.enableNextResponder) {
        [[self nextResponder] touchesBegan:touches withEvent:event];
        [super touchesBegan:touches withEvent:event];
    }
}

案例二

Window
  -ViewA(能響應(yīng))
    -ButtonA
    -ViewB(不響應(yīng))

假設(shè)ViewB完全覆蓋在ButtonA上,結(jié)果是:
ViewA能觸發(fā)
Button沒反應(yīng)
ViewB沒反應(yīng)

簡(jiǎn)單來說,ViewB能阻隔ButtonA的響應(yīng),但是不能阻隔ViewA的響應(yīng)。假設(shè)ViewB是個(gè)遮罩,那么并不能阻隔ViewA的事件觸發(fā)。假設(shè)需要阻隔ViewA的相應(yīng),那么可以在ViewB添加空事件的相應(yīng),從而達(dá)到ViewB(遮罩)阻隔ViewA(如游戲?qū)樱┑氖录鄳?yīng)。

案例三

一個(gè)按鈕添加了點(diǎn)擊事件到底發(fā)生了什么事兒。
我們有時(shí)候需要使用到一些特殊的情況,比如:
1、A包含B,AB都響應(yīng)事件。
對(duì)于普通View,根據(jù)響應(yīng)鏈,讓B作為第一個(gè)響應(yīng)者處理,然后B根據(jù)nextResponder傳遞觸摸事件。
針對(duì)手勢(shì)做分析:
手勢(shì)不會(huì)走view的touches系列方法,但有自己的一系列touches方法,不過沒有暴露出來。但是shouldRecognizeSimultaneouslyWithGestureRecognizer也可以做到。

針對(duì)UIButton的分析:
UIButton addTarget分析,addTarget是UIControl的方法,其實(shí)addTarget的方法原理是,UIControl對(duì)touches的觸摸事件的封裝。[super touchesBegan:touches withEvent:event];包括了對(duì)
事件的封裝處理,如果重新了[super touchesBegan:touches withEvent:event];,并且里面什么都不實(shí)現(xiàn),那么當(dāng)前UIButton添加的addTarget所綁定的所有事件都不會(huì)觸發(fā)。因?yàn)楦采w了父類UIControl的封裝方法。
如果我想一個(gè)按鈕的事件觸發(fā),并且它的下一響應(yīng)者也能觸發(fā)相應(yīng)的事件。那么該怎么處理呢?
我們?cè)诎粹o上處理,重寫touchesBegan系列的方法,那么根據(jù)上面所說,必須要調(diào)用super的方法,并且主動(dòng)像下一響應(yīng)者[self nextResponder]發(fā)送touchesBegan系列的方法。

2、A包含B、C,C在B的上面,但是想讓B接收事件,C不接收事件
這種可以這么處理,自定義C的View,重寫hitTest:withEvent方法,返回nil,這樣自定義C的View及其子類都不會(huì)攔截事件。這樣B就可以順利處理事件。
還可以把C的userInteractionEnabled設(shè)置為NO

3、A是B、C的父視圖,C在B的上面,這時(shí)候,CB都處理事件。這樣到底行不行?根據(jù)響應(yīng)鏈,這樣應(yīng)該是不靠譜的了。在C的touches方法中調(diào)用C的touches方法,然后重寫B(tài)的touches方法,但是這樣怪怪的。有什么高招也請(qǐng)多多指教。貌似也沒有這樣的必要。

最后還發(fā)現(xiàn)了一篇一步到位的iOS響應(yīng)者鏈的全過程:iOS觸摸事件的流動(dòng)(想有更清晰的了解的看)
直接引用里面的一張圖:

image.png

參考資料:
響應(yīng)者鏈及相關(guān)機(jī)制總結(jié)

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

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