事件的生命周期是:
- 事件的產(chǎn)生(發(fā)生觸摸等)
- 事件的傳遞(找到最適合處理事件的控件)
- 事件的響應(yīng)(處理事件)
響應(yīng)者對(duì)象(UIResponder)
在iOS中不是任何對(duì)象都能處理事件振乏,只有繼承了UIResponder的對(duì)象才能接受并處理事件澄干,我們稱之為“響應(yīng)者對(duì)象”。
以下都是繼承自UIResponder的脯丝,所以都能接收并處理事件崭孤。
UIApplication
UIViewController
UIView
那么為什么繼承自UIResponder的類就能夠接收并處理事件呢胀溺?
因?yàn)閁IResponder中提供了以下4個(gè)方法來(lái)處理觸摸事件厘唾。
// 開(kāi)始觸摸
// 若是兩根手指同時(shí)觸摸,view調(diào)用一次touchesBegan方法鹃操,且touches內(nèi)包含兩個(gè)touchu對(duì)象
// 若是兩根手指一前一后觸摸韭寸,view調(diào)用兩次次touchesBegan方法,且touches只包含一個(gè)touchu對(duì)象
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//開(kāi)始移動(dòng)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//結(jié)束觸摸荆隘,離開(kāi)view
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//結(jié)束觸摸錢恩伺,某個(gè)系統(tǒng)事件(例如電話呼入)會(huì)打斷觸摸過(guò)程巾钉,系統(tǒng)會(huì)自動(dòng)調(diào)用cancel方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
以上四個(gè)方法是由系統(tǒng)自動(dòng)調(diào)用的牵啦,可以通過(guò)重寫該方法來(lái)處理一些事件吗冤。
重寫這些方法必須是自定義view的類中實(shí)現(xiàn)柬甥,因?yàn)橄到y(tǒng)沒(méi)有給我們UIView.m的源碼,我們只能通過(guò)子類繼承父類實(shí)現(xiàn)叶组。(此處強(qiáng)調(diào)的是UIView壳繁,不是UIViewCOntroller)
重寫UIViewController的觸摸事件坟募,可以直接在控制器的.m文件中重寫這些方法即可缆毁。
UIRespond還有加速計(jì)事件番川、遠(yuǎn)程控制事件的處理,此處不展開(kāi)描述。
加速計(jì)事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠(yuǎn)程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
UITouch對(duì)象
當(dāng)用戶手指觸摸屏幕的時(shí)候爽彤,會(huì)創(chuàng)建一個(gè)與手指相關(guān)的Touch對(duì)象。
作用:
1缚陷、保存著跟手指相關(guān)的信息适篙,比如觸摸的位置、時(shí)間箫爷、階段等嚷节。
2、當(dāng)手指移動(dòng)時(shí)虎锚,系統(tǒng)會(huì)更新同一個(gè)UITouch對(duì)象硫痰,使之能夠一直保存該手指在的觸摸位置。
3窜护、當(dāng)手指離開(kāi)屏幕時(shí)效斑,系統(tǒng)會(huì)銷毀相應(yīng)的UITouch對(duì)象。
事件的產(chǎn)生
當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后柱徙,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中缓屠。
為什么是隊(duì)列而不是棧?
因?yàn)殛?duì)列的特點(diǎn)是FIFO护侮,即先進(jìn)先出敌完,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊(duì)列羊初。
事件的傳遞
事件發(fā)生并加入到UIApplication管理的事件隊(duì)列中后滨溉,UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理长赞,一般是先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)晦攒。主窗口會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件。
觸摸事件的傳遞是從父控件傳遞到子控件
也就是UIApplication->window->尋找處理事件最合適的view注意:如果父控件不能接受觸摸事件得哆,那么子控件就不可能接收到觸摸事件
如何找到最合適的控件來(lái)處理事件
1勤家、首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
不能接受觸發(fā)事件的原因:
1、不允許交互:userInteractionEnabled = NO
2柳恐、隱藏:如果把父控件隱藏伐脖,那么子控件也會(huì)隱藏,隱藏的控件不能接受事件
3乐设、透明度:如果設(shè)置一個(gè)控件的透明度<0.01讼庇,會(huì)直接影響子控件的透明度。alpha:0.0~0.01為透明近尚。注意:默認(rèn)UIImageView不能接受觸摸事件蠕啄,因?yàn)椴辉试S交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互歼跟,需要設(shè)置UIImageView的userInteractionEnabled = YES和媳。
2、判斷觸摸點(diǎn)是否在自己身上哈街。
3留瞳、子控件數(shù)組中從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟骚秦。
所謂從后往前遍歷子控件她倘,就是首先查找子控件數(shù)組中最后一個(gè)元素,然后執(zhí)行1作箍、2步驟硬梁,(采取倒序遍歷子控件的方式尋找最合適的view是為了做一些循環(huán)優(yōu)化,因?yàn)橄啾容^之下胞得,后添加的view在上面荧止,降低循環(huán)次數(shù)。)
4阶剑、view罩息,比如叫做hitView,那么會(huì)把這個(gè)事件交給這個(gè)hitView个扰,再遍歷這個(gè)hitView的子控件瓷炮,直至沒(méi)有更合適的view為止。
5递宅、如果沒(méi)有符合條件的子控件娘香,那么就認(rèn)為自己最合適處理這個(gè)事件,也就是自己是最合適的view办龄。
兩個(gè)重要的方法:
1烘绽、-(nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
只要事件一傳遞給一個(gè)控件,這個(gè)控件就會(huì)調(diào)用它自己的hitTest:withEvent:方法,目的是返回最適合的view俐填。
可以通過(guò)重寫這個(gè)方法安接,返回指定的view作為最合適的view。
如果hitTest:withEvent:方法中返回nil英融,那么調(diào)用該方法的控件本身和其子控件都不是最合適的view盏檐,也就是在自己身上沒(méi)有找到更合適的view。那么最合適的view就是該控件的父控件驶悟。
事件的傳遞順序是這樣的:
產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view胡野。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判斷下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判斷下點(diǎn)在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 設(shè)置一個(gè)初始視圖
__block UIView *hitView = nil;
// 遍歷子視圖
// NSEnumerationReverse 倒序
[self.subviews enumerateObjectsWithOptions: NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CGPoint p = [self convertPoint:point toView:obj];
hitView = [obj hitTest:p withEvent:event];
if (hitView) {
*stop = YES;
}
}];
if (hitView){
return hitView;
}
// 4.沒(méi)有找到更合適的view,也就是沒(méi)有比自己更合適的view
return self;
}
2痕鳍、-(BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
判斷點(diǎn)在不在當(dāng)前view上(方法調(diào)用者的坐標(biāo)系上)如果返回YES硫豆,代表點(diǎn)在方法調(diào)用者的坐標(biāo)系上龙巨;返回NO代表點(diǎn)不在方法調(diào)用者的坐標(biāo)系上,那么方法調(diào)用者也就不能處理事件熊响。
如果要修改點(diǎn)擊區(qū)域旨别,核心方法就是修改上述兩個(gè)方法。通過(guò)修改這兩個(gè)方法汗茄,根據(jù)需求修改響應(yīng)區(qū)域范圍即可秸弛。
事件傳遞流程圖
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 內(nèi)部實(shí)現(xiàn)流程圖
先校驗(yàn)view是不是隱藏,是不是可以觸發(fā)點(diǎn)擊剔难,是不是透明度大于0.01胆屿,條件全部滿足就繼續(xù)校驗(yàn)奥喻,否則是返回到父視圖繼續(xù)遍歷調(diào)用其兄弟視圖偶宫。
再校驗(yàn)點(diǎn)是不是在此view上,是的話開(kāi)始倒序遍歷自己的子視圖环鲤,不是的話纯趋,也會(huì)返回到父視圖,繼續(xù)遍歷其兄弟視圖冷离。
最終若找到就返回view吵冒,沒(méi)找到就返回nil。
事件響應(yīng)
事件從UIApplication->UIWindow->尋找處理事件最合適的view傳遞后
然后就會(huì)調(diào)用控件的touches方法來(lái)作具體的事件處touchesBegan…touchesMoved…touchedEnded…等西剥。
touches 方法就是對(duì)事件的響應(yīng)痹栖,事件的響應(yīng)是順著響應(yīng)鏈向上傳遞的,這個(gè)傳遞是依賴于UIResponder的nextResponder瞭空。
UIView :如果view是VC的root view揪阿,則它的nextResponder是 VC;否則是父view
UIViewController :如果 vc 是window的root vc咆畏,則它的nextResponder是 window南捂,否則是父vc
UIWindow:它的nextResponder是UIApplication
UIApplication :它的nextResponder是app delegate。
整個(gè)事件在找到合適的view之后旧找,判斷當(dāng)前view是否能處理這個(gè)事件溺健,如果不能,則順著nextResponder向父view傳遞钮蛛,如果傳遞到VC也不能處理這個(gè)事件鞭缭,則繼續(xù)傳遞到UIWindow,如果window對(duì)象也不處理魏颓,則其將事件或消息傳遞給UIApplication對(duì)象缚去,如果UIApplication也不能處理該事件或消息,則將其丟棄琼开。其中任何一環(huán)能處理事件易结,則進(jìn)行時(shí)間處理,整個(gè)事件的傳遞就結(jié)束了。
視圖事件響應(yīng)
-(void)touchesBegan:(NSSet*)touches withEvent:(UIevent *)event;
-(void)touchesMoved:(NSSet*)touches withEvent:(UIevent *)event;
-(void)touchesended:(NSSet*)touches withEvent:(UIevent *)event;
總結(jié)
事件處理的整個(gè)流程總結(jié):
- 觸摸屏幕產(chǎn)生觸摸事件后搞动,觸摸事件會(huì)被添加到由UIApplication管理的事件隊(duì)列中(即躏精,首先接收到事件的是UIApplication)。
- UIApplication會(huì)從事件隊(duì)列中取出最前面的事件鹦肿,把事件傳遞給應(yīng)用程序的主窗口(keyWindow)矗烛。
- key window會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件。(至此箩溃,第一步已完成)
- 最合適的view會(huì)調(diào)用自己的touches方法處理事件瞭吃。
- touches默認(rèn)做法是把事件順著響應(yīng)者鏈條向上拋,即順著nextResponder向上傳遞涣旨。
事件的傳遞和響應(yīng)的區(qū)別:
事件的傳遞是從上到下(父控件到子控件)歪架。
事件的響應(yīng)是從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件)。
應(yīng)用實(shí)踐
1霹陡、指定區(qū)域響應(yīng)事件
僅紅色區(qū)域內(nèi)可以點(diǎn)擊
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 計(jì)算點(diǎn)擊位置與按鈕中心點(diǎn)的距離
CGFloat distance = sqrt(pow(point.x - CGRectGetMidX(self.bounds), 2) + pow(point.y - CGRectGetMidY(self.bounds), 2));
// 判斷距離是否小于圓形半徑
CGFloat radius = MIN(self.bounds.size.width, self.bounds.size.height) * 0.4;
return distance <= radius;
}
- 具體的判斷條件根據(jù)需求設(shè)定和蚪,還可以擴(kuò)大/縮小點(diǎn)擊范圍
2、表述下圖事件傳遞和響應(yīng)流程
傳遞過(guò)程:
屏幕點(diǎn)擊這個(gè)位置烹棉,這個(gè)事件傳遞UIApplication
UIApplication傳遞給UIWindow(UIWindow也是一個(gè)視圖)
從UIWindow里面開(kāi)始執(zhí)行hitTest方法查找最適合的視圖攒霹。
hitTest方法內(nèi)部調(diào)用pointInside方法判斷點(diǎn)擊的位置是不是在視圖范圍內(nèi)
如果是,
則倒序遍歷子視圖浆洗,(后添加的view在上面催束,降低循環(huán)次數(shù)),返回合適的view嗎伏社,再在此view及其子視圖繼續(xù)調(diào)用hitTest方法抠刺,直到找到最適合的view。
如果不是洛口,返回UIWindow矫付。根據(jù)圖片標(biāo)注的view名稱:
ViewA->ViewB2->ViewD->ViewD中白色區(qū)域
響應(yīng)過(guò)程:
找到最適合的view后,開(kāi)始調(diào)用touches事件
touches默認(rèn)做法是把事件順著響應(yīng)鏈條向上拋第焰,也就是順著nextResponder向上傳遞买优。
結(jié)合圖中名稱:
點(diǎn)擊的白色區(qū)域有ViewD響應(yīng)
若ViewD不響應(yīng)則由其父視圖ViewB2去響應(yīng)
若ViewB2不響應(yīng)則由其父視圖ViewA去響應(yīng)
若ViewA不響應(yīng)則順著nextResponder繼續(xù)查找直到UIApplication->UIApplicationDelegate。
如果整個(gè)過(guò)程中都沒(méi)有響應(yīng)挺举,則忽略此事件杀赢,app不會(huì)任何反應(yīng)。