平時(shí)開發(fā)中經(jīng)常會(huì)遇到子view超出父view時(shí),超出的部分不會(huì)響應(yīng)點(diǎn)擊事件
原因就在于:iOS的事件響應(yīng)機(jī)制
接下來(lái)簡(jiǎn)單了解一下事件的響應(yīng)鏈和傳遞鏈
響應(yīng)鏈流程
基本流程
大家都知道 iOS 的響應(yīng)鏈?zhǔn)?UIApplication 收到用戶觸摸屏幕的事件以后通過(guò)逐層尋找最后得到用戶觸摸的 View 也就是第一響應(yīng)者,然后調(diào)用 View 的 touchesBegan:withEvent:
方法處理事件任務(wù)的流程.大概流程是這樣的:
圖片很清晰的說(shuō)明了查找流程 AppDelegate 收到事件逐層查找.最終找到 UIButton 這個(gè)響應(yīng)者 然后調(diào)用 UIButton 的
touchesBegan:withEvent:
方法處理事件.可以總結(jié)為一下幾點(diǎn)
一個(gè)觸摸事件的響應(yīng)過(guò)程如下:
- 用戶觸摸屏幕時(shí)瑰抵,UIKit會(huì)生成UIEvent對(duì)象來(lái)描述觸摸事件。對(duì)象內(nèi)部包含了觸摸點(diǎn)坐標(biāo)等信息谅河。
- 通過(guò)Hit Test確定用戶觸摸的是哪一個(gè)UIView雄卷。這個(gè)步驟通過(guò)- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法來(lái)完成。
- 找到被觸摸的UIView之后谴蔑,如果它能夠響應(yīng)用戶事件冕象,相應(yīng)的響應(yīng)函數(shù)就會(huì)被調(diào)用代承。如果不能響應(yīng),就會(huì)沿著響應(yīng)鏈(Responder Chain)尋找能夠響應(yīng)的UIResponder對(duì)象(UIView是UIResponder的子類)來(lái)響應(yīng)觸摸事件渐扮。
如何查找第一響應(yīng)者
查找第一響應(yīng)者主要涉及以下兩個(gè)方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
系統(tǒng)會(huì)先執(zhí)行- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
如果不在響應(yīng)的point內(nèi)的話直接返回 false,接下來(lái)- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
會(huì)直接返回nil
pointInside:
通過(guò) point 參數(shù)確定觸碰點(diǎn)是否在當(dāng)前 View 的響應(yīng)范圍內(nèi) 是則返回YES 否則返回 NO 實(shí)現(xiàn)方法大概是這個(gè)樣子的
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.bounds, point);
}
hitTest方法:
- 它首先會(huì)通過(guò)調(diào)用自身的 pointInside 方法判斷用戶觸摸的點(diǎn)是否在當(dāng)前對(duì)象的響應(yīng)范圍內(nèi),如果 pointInside 方法返回 NO hitTest方法直接返回 nil
- 如果 pointInside 方法返回 YES hitTest方法接著會(huì)判斷自身是否有子視圖.如果有則調(diào)用頂層子視圖的 hitTest 方法 直到有子視圖返回 View
- 如果所有子視圖都返回 nil hitTest 方法返回自身.
尋找 hit-TestView 的過(guò)程的總結(jié)
通過(guò) hit-Testing 找到觸摸點(diǎn)所在的 View( hit-TestView )论悴。尋找過(guò)程總結(jié)如下(默認(rèn)情況下) :
- 尋找順序如下
- 從視圖層級(jí)最底層的 window 開始遍歷它的子 View。
- 默認(rèn)的遍歷順序是按照 UIView 中 Subviews 的逆順序墓律。
- 找到 hit-TestView 之后膀估,尋找過(guò)程就結(jié)束了。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
return nil;
} else {
// 判斷觸摸位置是否在當(dāng)前視圖內(nèi)
if ([self pointInside:point withEvent:event]) {
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個(gè)視圖開始查找
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;
}
}
// 所有子視圖都沒(méi)有返回 則返回自身
return self;
//for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
// UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
// if (hitView) {
// return hitView;
//}
} else {
return nil;
}
}
}
根據(jù)以上源碼分析
確定一個(gè) View 是不是 hit-TestView 的過(guò)程如下:
- 如果 View 的 userInteractionEnabled = NO耻讽,enabled = NO( UIControl )察纯,或者 alpha <= 0.01, hidden = YES 直接返回 nil(不再往下判斷)。
- 如果觸摸點(diǎn)不在 view 中饼记,直接返回 nil香伴。
- 如果觸摸點(diǎn)在 view 中,逆序遍歷它的子 View 具则,重復(fù)上面的過(guò)程瞒窒,如果子View沒(méi)有subView了,那子View就是hit-TestView乡洼。
- 如果 view 的 子view 都返回 nil(都不是 hit-TestVeiw ),那么返回自身(自身是 hit-TestView )匕坯。
圖來(lái)自https://liangdahong.com/2018/06/08/2018/淺談-iOS-事件的傳遞和響應(yīng)過(guò)程/
看過(guò)這個(gè)束昵,我們也就懂了為什么超出屏幕不能響應(yīng)點(diǎn)擊事件-----如果超出邊界,UIKit無(wú)法根據(jù)這個(gè)觸摸點(diǎn)找到父視圖葛峻,自然也就無(wú)法找到子視圖锹雏。尋找過(guò)程有點(diǎn)像遞歸,可以理解理解术奖。
找到 hit-TestView 之后礁遵,事件就交給它來(lái)處理,hit-TestView 就是 firstResponder(第一響應(yīng)者)采记,如果它無(wú)法響應(yīng)事件(不處理事件)佣耐,則把事件交給它的 nextResponder(下一個(gè)響應(yīng)者)
第一響應(yīng)者在這幾個(gè)方法中處理響應(yīng)的事件,處理完成后根據(jù)需要調(diào)用 nextResponder 的 touch 方法,通常 nextResponder 就是第一響應(yīng)者的 superView 文章的第一張圖倒著看就是nextResponder 的順序
nextResponder 過(guò)程如下:
當(dāng)一個(gè)view被add到superView上的時(shí)候,他的nextResponder屬性就會(huì)被指向它的superView唧龄,當(dāng)controller被初始化的時(shí)候兼砖,self.view(topmost view)的nextResponder會(huì)被指向所在的controller,而controller的nextResponder會(huì)被指向self.view的superView既棺,這樣讽挟,整個(gè)app就通過(guò)nextResponder串成了一條鏈,也就是我們所說(shuō)的響應(yīng)鏈,他只是一條虛擬鏈丸冕,所以總結(jié)各個(gè)可響應(yīng)者的nextResponder如下:
- UIView 的 c 是直接管理它的 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洪己。
基于以上過(guò)程妥凳,那么上面尋找hit_testView例子的響應(yīng)者鏈就是viewB.1-viewB-mainView-UIWindow
有了響應(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。
接下來(lái)我們看一下事件的傳遞(傳遞鏈)
找到第一響應(yīng)者 application 便會(huì)根據(jù) event 調(diào)用第一響應(yīng)者響應(yīng)的
touch 方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
當(dāng)我們點(diǎn)擊了Button之后哗咆,UIWindow會(huì)通過(guò)一個(gè)私有方法蜘欲,在里面會(huì)去調(diào)用按鈕的touchesBegan和touchesEnded方法,touchesBegan里面有設(shè)置按鈕的高亮等之類的動(dòng)作晌柬,這樣就實(shí)現(xiàn)了事件的傳遞姥份。而事件的響應(yīng),也就是按鈕上綁定的action年碘,是在touchEnded里面通過(guò)調(diào)用UIApplication的sendAction:to:from:forEvent:方法來(lái)實(shí)現(xiàn)的澈歉,至于這個(gè)方法里面是怎么去響應(yīng)action,就只能猜測(cè)了(可能是通過(guò)oc底層消息機(jī)制的相關(guān)接口
objc_msgSend
來(lái)發(fā)送消息實(shí)現(xiàn)的屿衅,可以參考message.h文件)埃难。如果第一響應(yīng)者沒(méi)有響應(yīng)這個(gè)事件,那么就會(huì)根據(jù)響應(yīng)鏈涤久,把事件冒泡傳遞給nextResponder來(lái)響應(yīng)涡尘。
注意這里是怎么把事件傳遞給nextResponder的呢?拿touch事件來(lái)說(shuō)响迂,UIResponder里面touch四個(gè)階段的方法里面考抄,實(shí)際上是什么事都沒(méi)有做的,UIView繼承了它進(jìn)行重寫蔗彤,重寫的內(nèi)容也是沒(méi)有什么東西座泳,就是把事件傳遞給nextResponder,UIView繼承于 UIResponder,而UIResponder只是重寫了touch 四個(gè)方法幕与,真正的實(shí)現(xiàn)在UIControl里挑势,可以看UIKit源碼
UIControl->UiView->UIResponder->NSObject這個(gè)繼承關(guān)系
UIResponder有touch的四個(gè)方法,而實(shí)現(xiàn)都在UIControl
UIResponder中touch方法實(shí)現(xiàn)如下啦鸣,默認(rèn)不做任何操作潮饱,只是沿著響應(yīng)鏈傳遞事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self nextResponder] touchesBegan:touches withEvent:event];
}
所以當(dāng)一個(gè)view或者controller里面沒(méi)有重寫touch事件,那么這個(gè)事件就會(huì)一直傳遞下去诫给,直到UIApplication香拉,這也就是事件往上冒泡的原理。如果view重寫了touch方法中狂,我們一般會(huì)看到的效果是凫碌,這個(gè)view響應(yīng)了事件之后,事件就被截?cái)嗔宋搁牛膎extResponder不會(huì)收到這個(gè)事件盛险,即使重寫了nextResponder的touch方法。這個(gè)時(shí)候如果想事件繼續(xù)傳遞下去,可以調(diào)用[super
touchesBegan:touches withEvent:event]苦掘,不建議直接調(diào)[self.nextResponder
touchesBegan:touches withEvent:event]换帜。
注意這里是怎么把事件傳遞給nextResponder的呢?拿touch事件來(lái)說(shuō)鹤啡,UIResponder里面touch四個(gè)階段的方法里面惯驼,實(shí)際上是什么事都沒(méi)有做的,UIView繼承了它進(jìn)行重寫递瑰,重寫的內(nèi)容也是沒(méi)有什么東西祟牲,就是把事件傳遞給nextResponder,UIView繼承于 UIResponder,而UIResponder只是重寫了touch 四個(gè)方法抖部,真正的實(shí)現(xiàn)在UIControl里疲眷,可以看UIKit源碼
UIControl->UiView->UIResponder->NSObject這個(gè)繼承關(guān)系
UIResponder有touch的四個(gè)方法,而實(shí)現(xiàn)都在UIControl
UIResponder中touch方法實(shí)現(xiàn)如下您朽,默認(rèn)不做任何操作,只是沿著響應(yīng)鏈傳遞事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self nextResponder] touchesBegan:touches withEvent:event];
}
剛才提到UIControl
在.h中能看到特別像touch四個(gè)方法的四個(gè)方法Tracking系列
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = YES;
_tracking = [self beginTrackingWithTouch:touch withEvent:event];
self.highlighted = YES;
if (_tracking) {
UIControlEvents currentEvents = UIControlEventTouchDown;
if (touch.tapCount > 1) {
currentEvents |= UIControlEventTouchDownRepeat;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
根據(jù)上面的源碼看到 touch內(nèi)部調(diào)用了Tracking一系列的 touch 方法
如果你點(diǎn)擊UIButton的響應(yīng)事件的話换淆,打個(gè)斷點(diǎn)哗总,看到方法調(diào)用
就直接是UIApplication 調(diào)用sendAction方法,相當(dāng)于直接到鏈頭了吧
首先劃重點(diǎn):所有響應(yīng)者的基類都是 UIResponder倍试,UIApplication/UIView/UIViewController 都是 UIResponder 的子類
尋找響應(yīng)者過(guò)程如下:
當(dāng)我們知道最合適的 View 后讯屈,事件會(huì) 由上向下【子view -> 父view,控制器view -> 控制器】來(lái)找出合適響應(yīng)事件的 View县习,來(lái)響應(yīng)相關(guān)的事件涮母。那怎么就是真正的響應(yīng)者呢?
如果當(dāng)前的 View 有添加手勢(shì)躁愿,那么直接響應(yīng)相應(yīng)的事件叛本,不會(huì)繼續(xù)向下尋找了,因?yàn)槭謩?shì)比響應(yīng)鏈擁有更高的優(yōu)先級(jí)
如果沒(méi)有手勢(shì)事件彤钟,那么會(huì)看其是否實(shí)現(xiàn)了如下的方法:
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
經(jīng)過(guò)驗(yàn)證一二步是并行的来候,甚至touchBegin響應(yīng)更早一丟丟,所以只要有以上兩個(gè)之一實(shí)現(xiàn)逸雹,都會(huì)阻斷響應(yīng)鏈傳遞
舉例
藍(lán)色圓加在紅色方塊上营搅,重疊部分如果想響應(yīng)紅色塊的點(diǎn)擊事件,應(yīng)該怎么辦呢
- 重寫藍(lán)色圓的hiteTest
//self.btn為紅色塊
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
//藍(lán)色的點(diǎn)轉(zhuǎn)換給紅色
CGPoint redBtnPoint = [self convertPoint:point toView:self.btn];
if([self.btn pointInside:redBtnPoint withEvent:event]) {
return _btn;
}
return view;
}
事件攔截
通常第一響應(yīng)者都是響應(yīng)鏈中最末端的響應(yīng)者,事件攔截就是在響應(yīng)鏈中截獲事件,停止下發(fā).將事件交由中間的某個(gè)響應(yīng)者執(zhí)行.比如這樣:
通常點(diǎn)擊紅色 view 事件將交由 紅色 view 處理.如果想讓粉色 View 或者綠色 view 處理事件應(yīng)該怎么辦?
有兩種辦法
- 在紅色 view 的的 touch 方法中調(diào)用父類或者 nextResponder 的
touch 方法 - 在需要攔截的 view 中重寫 hitTest 方法改變第一響應(yīng)者
首先來(lái)看第一種
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 將事件傳遞給下一響應(yīng)者
[self.nextResponder touchesBegan:touches withEvent:event];
// 調(diào)用父類的touch方法 和上面的方法效果一樣 這兩句只需要其中一句
[super touchesBegan:touches withEvent:event];
}
這種方法有兩個(gè)問(wèn)題,你需要重寫所有的 touch 方法并且還要重寫要攔截事件的 view 與頂級(jí) view 之間的所有 view 的 touch 方法
第二種方法
重寫攔截事件的 view 的 hitTest 方法 比如要讓綠色的 view 處理事件 就重寫綠色 view 的 hitTest 方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 如果在當(dāng)前 view 中 直接返回 self 這樣自身就成為了第一響應(yīng)者 subViews 不再能夠接受到響應(yīng)事件
if ([self pointInside:point withEvent:event]) {
return self;
}
return nil;
}
這種方法比較簡(jiǎn)單粗暴.實(shí)現(xiàn)后 所有 subview 將不再能夠接受任何事件 具體使用那種方式看需求.當(dāng)然還可以通過(guò) event 或者 point 有針對(duì)性的攔截
事件轉(zhuǎn)發(fā)
有時(shí)候還需要將事件轉(zhuǎn)發(fā)出去.讓本來(lái)不能響應(yīng)事件的 view 響應(yīng)事件,最常用的場(chǎng)景就是讓子視圖超出父視圖的部分也能響應(yīng)事件,比如要實(shí)現(xiàn)這樣的 tabbar
橙色按鈕有兩個(gè)區(qū)域 a 區(qū)超出父視圖 b 區(qū)沒(méi)有超出父視圖,如果不作處理,那么點(diǎn)擊 a 區(qū)是無(wú)法響應(yīng)事件的,因?yàn)?a 區(qū)域的坐標(biāo)不在父視圖的范圍內(nèi),當(dāng)執(zhí)行到父視圖的 pointInside 的時(shí)候就會(huì)返回 NO
想要讓 a 區(qū)響應(yīng)事件 就需要重寫父視圖的 pointInside 或 hitTest 方法讓 pointInside 返回 YES 或 讓hitTest 直接返回橙色視圖
重寫hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 觸摸點(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è)視圖開始查找
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;
}
重寫 pointInside 方法原理相同 重點(diǎn)注意轉(zhuǎn)換坐標(biāo)系 就算他們不是一條響應(yīng)鏈上 也可以通過(guò)重寫 hitTest 方法轉(zhuǎn)發(fā)事件.原理相同的東西就不再寫了
參考地址http://www.reibang.com/p/db3518be5ebb
https://blog.csdn.net/qiangshuting/article/details/90317936