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ī)制(建議還不清楚的看)
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系列流程。
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)(想有更清晰的了解的看)
直接引用里面的一張圖: