項目中的問題
在前段時間的項目中漏峰,遇到了一個與響應(yīng)鏈相關(guān)的問題情竹。效果圖如下:
在默認(rèn)狀態(tài)下障陶,最下方有五個按鈕滋恬;當(dāng)點擊選中地圖上的單車后,五個按鈕會同時上移抱究,并且導(dǎo)航視圖也會跟著上移恢氯。如果是你你會如何去實現(xiàn)。
我的第一反應(yīng)就是鼓寺,將這些彼此有約束的按鈕都放在一個自定義視圖上勋拟,這樣,當(dāng)需要上移或者下移的時候妈候,只需要改變這個自定義視圖的frame即可敢靡。但是其實這樣是有問題的。
首先州丹,為了不讓這個自定義視圖遮蓋住下面的地圖醋安,我將該視圖的背景顏色改為clearColor
。運行起來后墓毒,界面上沒有問題,但是當(dāng)我在自定義視圖的透明區(qū)域滑動地圖的時候亲怠,發(fā)現(xiàn)地圖不會響應(yīng)我的滑動事件所计。原因是該視圖雖透明,但仍然遮蓋在了地圖上方团秽,所以地圖不會響應(yīng)主胧。雖然我知道原因,但是用戶可不知道习勤,當(dāng)他發(fā)現(xiàn)下面部分不能滑動地圖踪栋,就認(rèn)為是bug了。图毕。
我當(dāng)時的解決辦法
當(dāng)時的我對于響應(yīng)鏈的掌握處于知道是什么夷都,卻不會用的狀態(tài)。由于時間緊迫予颤,我只能采取一個“不太好”的方法囤官。
我創(chuàng)建了一個管理下方視圖的工具類,在工具類初始化的時候傳入控制器的view蛤虐,并在內(nèi)部添加各種按鈕党饮。
這種方法其實和在控制器中添加一個個按鈕沒什么差別,現(xiàn)在只是將添加按鈕的代碼放到了工具類中驳庭,讓控制器的代碼能少點刑顺。
通過響應(yīng)鏈來解決
在使用響應(yīng)鏈之前,得知道響應(yīng)鏈?zhǔn)窃趺垂ぷ鞯摹T诮酉聛淼奈恼轮卸滋茫視葘戫憫?yīng)鏈的相關(guān)知識荞驴,再寫如何用這些相關(guān)知識去解決上面的問題。
什么是響應(yīng)鏈
響應(yīng)者鏈條:在iOS程序中無論是最后面的UIWindow還是最前面的某個按鈕贯城,它們的擺放是有前后關(guān)系的熊楼,一個控件可以放到另一個控件上面或下面,那么用戶點擊某個控件時是觸發(fā)上面的控件還是下面的控件呢能犯,這種先后關(guān)系構(gòu)成一個鏈條就叫“響應(yīng)者鏈”鲫骗。也可以說,響應(yīng)者鏈?zhǔn)怯啥鄠€響應(yīng)者對象連接起來的鏈條踩晶。在iOS中響應(yīng)者鏈的關(guān)系可以用下圖表示:
需要注意:
- 如果當(dāng)前這個view是控制器的view,那么控制器就是上一個響應(yīng)者
- 如果當(dāng)前這個view不是控制器的view,那么父控件就是上一個響應(yīng)者
上述來源:史上最詳細的iOS之事件的傳遞和響應(yīng)機制-原理篇
響應(yīng)鏈?zhǔn)侨绾喂ぷ鞯模üぷ鞑襟E)
第一步执泰、事件的產(chǎn)生
- 當(dāng)點擊其中一個視圖的時候,系統(tǒng)會將該事件加入到一個由
UIApplication
管理的事件隊列中 - 取出隊列里的最前面事件渡蜻,分發(fā)處理
第二步术吝、事件的傳遞
- 獲取到需要處理的事件后,順著響應(yīng)鏈茸苇,向下查找最合適的視圖排苍。
第三步、事件的響應(yīng)
- 找到最合適的視圖后学密,會調(diào)用自己的touches方法處理事件淘衙。如果自身沒有做處理(也就是自身沒有重寫touches方法),那么會逆著響應(yīng)鏈腻暮,向上拋彤守,直到找到能響應(yīng)這個事件的視圖。如果最上頭的
UIApplication
也不能處理該事件或消息哭靖,則將其丟棄具垫。
實例講解
我用一個簡單的實例來講解。在控制器中试幽,添加若干個視圖筝蚕。如同所示:
當(dāng)我點擊視圖D的時候,會發(fā)生些什么事情呢抡草。
- 因為沒有別的事件需要處理饰及,會把這個點擊事件拿出來處理。
- 開始順著響應(yīng)鏈查找最合適的響應(yīng)視圖 這里就是D視圖
- D視圖如果能響應(yīng)這個事件就響應(yīng)康震,如果不能上拋燎含,到
UIApplication
后還是不能就丟棄。
這里有幾個問題需要說明腿短。
這里的響應(yīng)鏈?zhǔn)窃趺礃拥?/h3>
響應(yīng)鏈如下圖所示:
如何通過這個鏈條找到最合適的視圖
在說這個之間先得知道屏箍,響應(yīng)鏈里的每一個類都是繼承UIResponder
绘梦,而該類中有以下兩個方法:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
當(dāng)在找尋響應(yīng)視圖的時候,會先調(diào)用hitTest
方法赴魁,這個方法的用途是返回一個最合適的視圖卸奉。
在hitTest
方法內(nèi)部調(diào)用pointInside
方法,這個方法的作用是點擊的點是否在自身內(nèi)部颖御。
下面以實例說說:(因為UIApplication不好說榄棵,就以View A舉例子)
- 當(dāng)點擊視圖D以后,會先進入
View A
的hitTest
方法來尋找最合適的視圖潘拱。 - 在
hitTest
方法內(nèi)部先判斷是否View A
隱藏疹鳄、不能觸發(fā)事件或者透明度<0.01。如果是芦岂,這返回nil瘪弓,說明最合適的視圖不在View A
內(nèi)部;反之禽最,繼續(xù)往下腺怯。 - 調(diào)用
pointInside
方法,判斷點是否在自身內(nèi)部川无。如果不在內(nèi)部呛占,那么同樣返回nil;反之舀透,說明在View A
內(nèi)部栓票,繼續(xù)往下。 - 在
View A
的子視圖中愕够,倒著找。(也就是先找E 再找D中)
這樣可以試著寫出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]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
上述代碼來源:iOS事件響應(yīng)鏈中Hit-Test View的應(yīng)用
那么示例中佛猛,點擊D視圖后惑芭,是怎么調(diào)用方法的呢?
我重寫了A-E五個視圖的hitTest
和pointInside
方法继找。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"進入A_View---hitTest withEvent ---");
UIView * view = [super hitTest:point withEvent:event];
NSLog(@"離開A_View--- hitTest withEvent ---hitTestView:%@",view);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
NSLog(@"A_view--- pointInside withEvent ---");
BOOL isInside = [super pointInside:point withEvent:event];
NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
return isInside;
}
(ps:這里只需要調(diào)用super的對應(yīng)方法遂跟,就可以讓重寫沒有影響了。這句話什么意思呢:如果只是在重寫方法中打印數(shù)據(jù)婴渡,那么就不會繼續(xù)往下找了幻锁。而如果調(diào)用了super的對應(yīng)方法,也就是UIView
的方法边臼,就會繼續(xù)往下找哄尔,就不會影響到程序了。)
當(dāng)我點擊D后柠并,打印結(jié)果如下:
最后找到了D視圖岭接。
找到視圖后的響應(yīng)是怎么回事
再找到視圖后富拗,會調(diào)用這個視圖的touches
方法。當(dāng)這個視圖有重寫這些方法的時候鸣戴,就說明這個視圖能響應(yīng)本次事件啃沪,如果這個視圖不能響應(yīng),那么就會順著響應(yīng)鏈上拋窄锅,直到找到能響應(yīng)這個事件的響應(yīng)者创千。
舉個栗子:
還是上面的示例,當(dāng)我不寫D視圖的touches
的方法入偷,也就是D視圖不能響應(yīng)事件追驴,那么會將這次響應(yīng)上拋給C視圖,如果我重寫了C視圖的touches
方法后盯串,會調(diào)用C視圖的方法氯檐。
當(dāng)我點擊D視圖后,調(diào)用的是C視圖的touches
体捏,而當(dāng)我點擊E視圖后冠摄,調(diào)用的是E視圖的touches
。
解決開始的問題
下面就可以來通過響應(yīng)鏈解決開頭的問題了几缭。
最好的思路
我把開頭的需求簡化成了如下:
如果什么都不操作河泳,只是在tableview上放一個yellow view
,yellow view
上放兩個按鈕年栓,那么在黃色視圖上滾動tableview是沒有用的拆挥。
原因是這樣的:
- 在
yellow view
視圖上 上下滑動tableview,事件產(chǎn)生并開始處理某抓。 - 通過響應(yīng)鏈查找最合適的響應(yīng)視圖
- 先是控制器view纸兔,發(fā)現(xiàn)點擊位置在其內(nèi)部
- 再是tableview,發(fā)現(xiàn)點擊位置在其內(nèi)部
- 再是
yellow view
發(fā)現(xiàn)點擊位置在其內(nèi)部 - 最后是兩個按鈕否副,發(fā)現(xiàn)不在他們內(nèi)部汉矿,那么找到最合適的視圖為
yellow view
。
- 那么由
yellow view
來響應(yīng)本次事件备禀。
清楚過程后洲拇,只需要在進入yellow view
的hitTest
方法時,做一下處理曲尸,讓其不是合適的響應(yīng)視圖即可赋续。
具體如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event]; // 找到了最合適的視圖
if (hitView && hitView == self) { // 當(dāng)找到了并且是自己本身時候,返回nil另患,告訴上一級tableview纽乱,最合適的視圖不在我內(nèi)部
return nil;
}
return hitView;
}
邏輯如下:
- 進入控制器view,發(fā)現(xiàn)點擊位置在其內(nèi)部柴淘,那么往其子視圖中找
- 發(fā)現(xiàn)只有tableview迫淹,并且進入發(fā)現(xiàn)點擊位置在tableview中秘通,說明最合適的響應(yīng)視圖要么是tableview,要么是tableview的子視圖敛熬,在往tableview子視圖中找找肺稀。
- 發(fā)現(xiàn)了
yellow view
,發(fā)現(xiàn)了點擊位置在yellow view
中并且再找了其兩個子視圖按鈕后發(fā)現(xiàn)yellow view
正是這個最合適的視圖应民。這個時候如果再不處理话原,系統(tǒng)默認(rèn)會返回yellow view
,那么tableview就沒有機會響應(yīng)了诲锹。但是返回了nil繁仁,告訴tableview她渴,沒找到最合適的脾歇,那么tableview就成了最合適的響應(yīng)視圖膘怕。
至于為什么要判斷hitView == self
呢橡淑,因為如果這次點擊的是兩個按鈕,那么這里的hitView
就是按鈕了闷堡,如果仍然返回nil秒梅,那么按鈕的響應(yīng)也將無法觸發(fā)屋确。
最終效果:
另一個思路
這里還有另一個思路桥爽,那就是重寫yellow view
的pointInside
方法朱灿,因為hitTest
內(nèi)部會調(diào)用pointInside
來判斷點擊點是否是視圖內(nèi)部,如果不管三七二十一直接返回NO钠四,那么這個視圖將永遠不會作為最合適的視圖盗扒。因此,可以使用這個特性來實現(xiàn)效果:
判斷一下點擊點的位置缀去,如果是兩個按鈕的位置侣灶,就返回YES,如果點擊位置yellow view
的黃色區(qū)域缕碎,就返回NO炫隶。
利用這個思路,還可以給小的按鈕增加響應(yīng)熱區(qū)阎曹,給超出父視圖的視圖富裕響應(yīng)能力等。