事件的產(chǎn)生
- 當(dāng)有觸摸或者其他事件產(chǎn)生尊惰,將事件交由
IOKit.framework
處理。 -
IOKit.framework
將事件封裝成一個IOHIDEvent
對象,并通過mach port
傳遞給SpringBoad
面徽。 -
SpringBoard
會接收這個對象并通過mach port
轉(zhuǎn)發(fā)給當(dāng)前App
的進程腻要; - 喚醒
runloop
复罐,觸發(fā)了source1
回調(diào),其回調(diào)函數(shù)為__IOHIDEventSystemClientQueueCallback()
雄家。 -
source1
回調(diào)觸發(fā)source0
回調(diào)效诅,將接收到的IOHIDEvent
對象封裝成UIEvent
對象進行處理或分發(fā)。
注意:
SpringBoard
其實是一個標準的應(yīng)用程序趟济,這個應(yīng)用程序用來管理 iOS
的主屏幕乱投;
source1
是蘋果用來監(jiān)聽 mach port
傳來的系統(tǒng)事件的,source0
是用來處理用戶事件的顷编。
source1
收到系統(tǒng)事件后戚炫,會回調(diào) source0
,所以最終這些事件都是由 source0
處理的媳纬。
事件傳遞的流程
- 當(dāng)用戶點擊屏幕時双肤,會產(chǎn)生一個觸摸事件,系統(tǒng)會將該事件加入到一個由
UIApplication
管理的事件隊列中钮惠。 -
UIApplication
會從事件隊列中取出最前面的事件茅糜,并將事件分發(fā)下去以便處理,通常先發(fā)送事件給應(yīng)用程序的主窗口keyWindow
素挽。 - 主窗口會調(diào)用
hitTest:withEvent:
方法在視圖View
層次結(jié)構(gòu)中找到一個最合適的View
來處理觸摸事件限匣。 - 最終,這個觸摸事件交給主窗口的
hitTest:withEvent:
方法返回的視圖對象去處理毁菱。
注意:如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件锌历。
View 不能接收觸摸事件的三種情況:
- 不允許交互:
userInteractionEnabled = NO
贮庞; - 隱藏:如果把父控件隱藏,那么子控件也會隱藏究西,隱藏的控件不能接受事件窗慎;
- 透明度:如果設(shè)置一個控件的透明度<0.01,會直接影響子控件的透明度卤材,0.0~0.01 為透明遮斥。
注意:
默認 UIImageView
不能接受觸摸事件,因為不允許交互扇丛,即 userInteractionEnabled = NO
术吗。所以如果希望 UIImageView
可以交互,需要設(shè)置 UIImageView
的 userInteractionEnabled = YES
帆精。
如何找到最合適的控件來處理事件较屿?
- 首先判斷主窗口
keyWindow
自己是否能接受觸摸事件隧魄。 - 調(diào)用當(dāng)前視圖的
pointInside:withEvent:
方法判斷觸摸點是否在當(dāng)前視圖內(nèi)。 - 若
pointInside:withEvent:
方法返回NO
隘蝎,說明觸摸點不在當(dāng)前視圖內(nèi)购啄,則當(dāng)前視圖的hitTest:withEvent:
返回nil
。 - 若
pointInside:withEvent:
方法返回YES
嘱么,說明觸摸點在當(dāng)前視圖內(nèi)狮含,則遍歷當(dāng)前視圖的所有子視圖subviews
,調(diào)用子視圖的hitTest:withEvent:
方法重復(fù)前面的步驟曼振,子視圖的遍歷順序是從上到下几迄,即從subviews
數(shù)組的末尾向前遍歷,直到有子視圖的hitTest:withEvent:
方法返回非空對象或者全部子視圖遍歷完畢拴测。 - 若第一次有子視圖的
hitTest:withEvent:
方法返回非空對象乓旗,則當(dāng)前視圖的hitTest:withEvent:
方法就返回此對象,處理結(jié)束集索。 - 若所有子視圖的
hitTest:withEvent:
方法都返回nil
屿愚,則當(dāng)前視圖的hitTest:withEvent:
方法返回當(dāng)前視圖自身。
查找第一響應(yīng)者
hitTest:withEvent:方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
控件通過重寫 hitTest:withEvent:
方法务荆,來判斷點擊區(qū)域是否在視圖上妆距,是則返回 YES
,不是則返回 NO
函匕,尋找并返回最合適的 view
(能夠響應(yīng)事件的那個最合適的 view
)。
應(yīng)用程序接收到事件后盅惜,將事件交給 keyWindow
并轉(zhuǎn)發(fā)給根視圖,根視圖按照視圖層級逐級遍歷子視圖结啼,并且遍歷的過程中不斷判斷視圖范圍,并最終找到第一響應(yīng)者屈芜。
事件傳遞給窗口或控件的后郊愧,就遞歸調(diào)用 hitTest:withEvent:
方法尋找更合適的 view
井佑。
在 hitTest:withEvent:
方法中,會從上到下遍歷子視圖躬翁,并調(diào)用 subViews
的 pointInside:withEvent:
方法焦蘑,通過重寫 pointInside:withEvent:
方法,返回點擊區(qū)域是否在視圖上盒发。如果找到子視圖則不斷調(diào)用其 hitTest:withEvent:
方法喇肋,以此類推。
在 hitTest:withEvent: 方法中返回 nil 的含義:
在 hitTest:withEvent:
方法中返回 nil
的意思是調(diào)用當(dāng)前 hitTest:withEvent:
方法的 view
不是合適的 view
甚侣,子控件也不是合適的 view
间学,如果同級的兄弟控件也沒有合適的 view
,那么最合適的 view
就是父控件低葫。
pointInside:withEvent: 方法
pointInside:withEvent:
方法判斷子控件的點在不在當(dāng)前 view
上(方法調(diào)用者的坐標系上)如果返回 YES
,代表點在方法調(diào)用者的坐標系上嘿悬;返回 NO
代表點不在方法調(diào)用者的坐標系上,那么方法調(diào)用者也就不能處理事件窒盐。
查找第一響應(yīng)者傳遞過程:
- 如果當(dāng)前
view
是控制器的view
钢拧,那么控制器就是上一個響應(yīng)者,事件就傳遞給控制器源内; - 如果當(dāng)前
view
不是控制器的view
,那么父視圖就是當(dāng)前view
的上一個響應(yīng)者嗽交,事件就傳遞給它的父視圖颂斜。 - 在視圖層次結(jié)構(gòu)的最頂級視圖,如果也不能處理收到的事件或消息焚鲜,則其將事件或消息傳遞給
window
對象進行處理放前。 - 如果
window
對象也不處理,則其將事件或消息傳遞給UIApplication
對象凭语。 - 如果
UIApplication
也不能處理該事件或消息葱她,則將其丟棄似扔。
事件攔截
有時候想讓指定視圖來響應(yīng)事件搓谆,不再向其子視圖繼續(xù)傳遞事件豪墅,可以通過重寫 hitTest:withEvent:
方法。在執(zhí)行到方法后斩萌,直接將該視圖返回屏轰,而不再繼續(xù)遍歷子視圖,這樣響應(yīng)者鏈的終端就是當(dāng)前視圖霎苗。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return self;
}
實際開發(fā)中可能會遇到一些特殊的交互需求,需要定制視圖對于事件的響應(yīng)内狸。例如下面 Tabbar
的這種情況升敲,中間的圓形按鈕是底部 Tabbar
上的控件,而 Tabbar
是添加在控制器根視圖中的驴党。默認情況下我們點擊圖中紅色方框中按鈕的區(qū)域,會發(fā)現(xiàn)按鈕并不會得到響應(yīng)倔既。
很明顯鹏氧,圖中紅色方框中按鈕是添加在 Tabbar
上面的,但是圖中紅色方框中按鈕的位置又超出了 Tabbar
的區(qū)域实蓬,當(dāng)點擊紅色方框區(qū)域后吊履,會發(fā)現(xiàn)紅色方框得不到響應(yīng)。
分析:
- 生成的觸摸事件首先傳到了
UIWindow
酌伊,然后UIWindow
將事件傳遞給控制器的根視圖UILayoutContainerView
缀踪; -
UILayoutContainerView
判斷自己可以響應(yīng)觸摸事件虹脯,然后將事件傳遞給子視圖Tabbar
奏候; - 子視圖
Tabbar
判斷觸摸點并不在自己的坐標范圍內(nèi),因此返回nil
鼻由; - 這時
UILayoutContainerView
將事件傳遞其他子視圖UINavigationTransitionView
,UINavigationTransitionView
判斷自己可以響應(yīng)事件蔼紧,就將事件時間傳遞給其子視圖UIViewControllerWrapperView
狠轻; -
UIViewControllerWrapperView
判斷自己可以響應(yīng)事件,就將事件傳遞給子視圖UITableViewController
控制器的TableView
查吊; -
TableView
判斷自己可以響應(yīng)事件湖蜕,所以UITableViewController
控制器的TableView
就是第一響應(yīng)者;
整個過程昭抒,事件根本沒有傳遞到圖中紅色方框中按鈕;
因此我們需要做的就是修改 Tabbar
的 hitTest:withEvent:
函數(shù)里面判斷點擊位置是否在 Tabbar
坐標范圍的的判斷條件盗迟,也就是需要重寫 Tabbar
的
pointInside:withEvent:
方法熙含,判斷如果當(dāng)前觸摸坐標在圖中紅色方框中按鈕上面,就返回 YES
邮弹,否則返回 NO
蚓聘;這樣一來時間就會最終傳遞到圖中紅色方框中按鈕上面,來響應(yīng)事件或粮。
// TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 將觸摸點坐標轉(zhuǎn)換到在 circleButton 上的坐標
CGPoint pointTemp = [self convertPoint:point toView:_circleButton];
// 若觸摸點在 cricleButton 上則返回 YES
if ([_circleButton pointInside:pointTemp withEvent:event]) {
return YES;
}
// 否則返回默認的操作
return [super pointInside:point withEvent:event];
}
事件轉(zhuǎn)發(fā)
在開發(fā)過程中氯材,經(jīng)常會遇到子視圖顯示范圍超出父視圖的情況,這時候可以重寫該視圖的 pointInside:withEvent:
方法氢哮,將點擊區(qū)域擴大到能夠覆蓋所有子視圖。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
CGFloat inset = 45.0f - 78.0f;
CGRect touchRect = CGRectInset(self.bounds, inset, inset);
if (CGRectContainsPoint(touchRect, point)) {
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;
}