先來(lái)明確幾個(gè)概念
- 響應(yīng)者對(duì)象
可以進(jìn)行事件處理的對(duì)象。用戶進(jìn)行了某個(gè)操作,系統(tǒng)會(huì)將該操作包裝成一個(gè)Event事件對(duì)象交與響應(yīng)者對(duì)象進(jìn)行處理 - 事件傳遞
如果第一響應(yīng)者對(duì)象沒(méi)有處理該事件,系統(tǒng)就會(huì)將它轉(zhuǎn)發(fā)到下一個(gè)響應(yīng)者對(duì)象那里處理,這個(gè)尋找的過(guò)程就成為事件傳遞 - 事件隊(duì)列
操作被包裝成事件之后,會(huì)先放入當(dāng)前活動(dòng)的Application事件隊(duì)列中,單例的UIApplication會(huì)從事件隊(duì)列中依次取出事件并交給單例的UIWindow來(lái)處理,UIWindow對(duì)象首先會(huì)尋找此次操作的初始點(diǎn)所在的view - 響應(yīng)鏈
進(jìn)行事件傳遞的由一系列響應(yīng)者對(duì)象組成的層次結(jié)構(gòu)就稱為響應(yīng)者鏈 - 涉及到的方法 :hitTest:withEvent:
命中測(cè)試方法,該方法用于尋找第一響應(yīng)者的過(guò)程中。返回值類型是UIView,表示當(dāng)前事件觸發(fā)的點(diǎn)在哪個(gè)view的范圍內(nèi)。hitTest方法的返回值,表示當(dāng)前視圖層級(jí)最低層的符合條件的視圖。比如,Aview的子視圖是Bview,點(diǎn)擊了Bview,雖然這個(gè)點(diǎn)也在Aview范圍內(nèi),但是hitTest會(huì)繼續(xù)向它的子視圖Bview進(jìn)行查找,最終hitTest返回的是Bview
該方法的point參數(shù)是相對(duì)于接收者的坐標(biāo)系的,其內(nèi)部會(huì)首先調(diào)用pointInside:withEvent: 方法, - 涉及到的方法:pointInside:withEvent:
該方法的返回值類型為布爾型,判斷參數(shù)中的點(diǎn)是否在當(dāng)前所在view范圍內(nèi)。官方對(duì)該方法的注釋是:default returns YES if point is in bounds - 第一響應(yīng)者
如5所說(shuō),雖然Aview和Bview都可以是點(diǎn)擊事件的響應(yīng)者,但系統(tǒng)只會(huì)將該事件交與最底層的Bview進(jìn)行處理,此時(shí)Bview就稱為第一響應(yīng)者
事件處理的過(guò)程
先將事件對(duì)象由父控件傳遞給子控件,找到最合適的控件來(lái)處理這個(gè)事件。 調(diào)用最合適控件的touches….方法 如果調(diào)用了[super touches….];就會(huì)將事件順著響應(yīng)者鏈條往上傳遞,傳遞給上一個(gè)響應(yīng)者 接著就會(huì)調(diào)用上一個(gè)響應(yīng)者的touches….方法。
如果view是控制器的view,就傳遞給控制器;如不是,則將其傳遞給它的父視圖 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對(duì)象進(jìn)行處理 如果window對(duì)象也不處理,則其將事件或消息傳遞給UIApplication對(duì)象 如果UIApplication也不能處理該事件或消息,則該事件會(huì)被丟棄,不能被處理
另外,在遍歷子控件的時(shí)候,是從子控件數(shù)組中最后一個(gè)元素開(kāi)始使用hitTest進(jìn)行判斷
兩個(gè)方法的內(nèi)部實(shí)現(xiàn)
大概的內(nèi)部實(shí)現(xiàn),只是為了方便理解
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *touchView = self;
// 事件發(fā)生點(diǎn)在當(dāng)前view范圍內(nèi),接著查詢是否在子view范圍內(nèi)
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in self.subviews) {
// 轉(zhuǎn)換下坐標(biāo),
CGPoint subPoint = CGPointMake(point.x - subView.frame.origin.x,
point.y - subView.frame.origin.y);
// 調(diào)用自身方法檢查子view
UIView *subTouchView = [subView hitTest:subPoint withEvent:event];
if (subTouchView) {
//找到touch事件對(duì)應(yīng)的view,停止遍歷
touchView = subTouchView;
break;
}
// 如果所有子視圖的hitTest都返回nil,則該方法直接返回self
}
}else{
//不在該View范圍內(nèi),直接返回nil,不會(huì)再繼續(xù)查詢子view的情況
touchView = nil;
}
return touchView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.bounds, point);
}
hitTest方法被忽略的情況
- 隱藏(hidden=YES)
- 禁止用戶操作(userInteractionEnabled=NO)
- alpha小于0.01
- 如果一個(gè)子視圖的區(qū)域超過(guò)父視圖的bound區(qū)域,雖然該子視圖內(nèi)容可以顯示,卻無(wú)法響應(yīng)事件
應(yīng)用
- 讓超出父view范圍的子view同樣可以響應(yīng)事件:超出范圍的子view之所以不能響應(yīng)事件,是因?yàn)楦敢晥D的pointInside方法返回了NO,只要重寫該方法,在方法內(nèi)部判斷需要響應(yīng)的范圍就可以了
- 同一個(gè)view,一部分范圍可以響應(yīng)事件,一部分范圍不可以響應(yīng)事件。比如要做一些不規(guī)則形狀的控件,控件之間有重合的部分,需求希望只有在點(diǎn)擊不重合的部分時(shí)才響應(yīng)事件,點(diǎn)擊重合部分不要有響應(yīng)。此時(shí)可以重寫該view的pointInside方法,同樣在內(nèi)部判斷響應(yīng)事件的范圍
- 讓一個(gè)view的子view都不能響應(yīng)事件:重寫該view的pointInside方法,直接返回NO
- 讓一個(gè)view自身以及它的子view都不能響應(yīng)事件:重寫該view的hitTest方法,直接返回nil(當(dāng)然有很多其他更簡(jiǎn)便的方式)