問(wèn)題
事件的產(chǎn)生和傳遞(事件如何從父控件傳遞到子控件并尋找到最合適的view、尋找最合適的view的底層實(shí)現(xiàn)、攔截事件的處理)
找到最合適的view后事件的處理(touches方法的重寫(xiě),也就是事件的響應(yīng))
-
其中重點(diǎn)和難點(diǎn)是:
- 1.如何尋找最合適的view
- 2.尋找最合適的view的底層實(shí)現(xiàn)(hitTest:withEvent:底層實(shí)現(xiàn))
iOS中的事件
- 觸摸事件 【本文只討論觸摸事件】
- 加速計(jì)事件
- 遠(yuǎn)程控制事件
響應(yīng)者對(duì)象(UIResponder)
在iOS中不是任何對(duì)象都能處理事件,只有繼承了UIResponder的對(duì)象才能接受并處理事件,我們稱之為“響應(yīng)者對(duì)象”。以下都是繼承自UIResponder的,所以都能接收并處理事件。UIViewController
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
- UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>
事件的處理往往需要 UITouch 對(duì)象
事件的產(chǎn)生和傳遞
- 事件的產(chǎn)生
點(diǎn)擊一個(gè)UIView或產(chǎn)生一個(gè)觸摸事件A,那么這個(gè)事件就產(chǎn)生了,這個(gè)觸摸事件A會(huì)被添加到由UIApplication管理的事件隊(duì)列中(即,首先接收到事件的是UIApplication,為什么是隊(duì)列而不是棧?因?yàn)殛?duì)列的特點(diǎn)是FIFO,即先進(jìn)先出,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊(duì)列)
- 事件的產(chǎn)生
-
- 事件的傳遞
2.1 觸摸事件的傳遞就是把事件傳遞到處理該事件最合適的view上
注 意: 如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件-
2.2 如何找到最合適的控件來(lái)處理事件
1)UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常,先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)2)判斷主窗口(keyWindow)自己是否能接受觸摸事件,如果能,則判斷觸摸點(diǎn)是否在keyWindow身上,如果在,則UIWindow將事件向下分發(fā),即UIView
3)判斷觸摸點(diǎn)是否在自己身上,若果在,怎執(zhí)行第4步
4)子控件數(shù)組中從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟(所謂從后往前遍歷子控件,就是首先查找子控件數(shù)組中最后一個(gè)元素,然后執(zhí)行3步驟)
4)view,比如叫做fitView,那么會(huì)把這個(gè)事件交給這個(gè)fitView,再遍歷這個(gè)fitView的子控件,直至沒(méi)有更合適的view為止。
6)如果沒(méi)有符合條件的子控件,那么就認(rèn)為自己最合適處理這個(gè)事件,也就是自己是最合適的view。
-
2.3 尋找最合適的view底層剖析
-
兩個(gè)最重要的方法
hitTest:withEvent:方法, pointInside方法- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
hitTest:withEvent 的說(shuō)明
- 調(diào)用的條件 :只要事件一傳遞給一個(gè)控件,那么這個(gè)控件就會(huì)調(diào)用自己的這個(gè)方法
-
作用 :尋找并返回最合適的view
能夠響應(yīng)事件的那個(gè)最合適的view
注 意:不管這個(gè)控件能不能處理事件,也不管觸摸點(diǎn)在不在這個(gè)控件上,事件都會(huì)先傳遞給這個(gè)控件,隨后再調(diào)用hitTest:withEvent:方法- 攔截事件的處理
正因?yàn)?code>hitTest:withEvent:方法 的返回值為:響應(yīng)事件的那個(gè)最合適的view,所以,通過(guò) 復(fù)寫(xiě) 該方法可以 攔截事件
新建一個(gè)MView,在.m 文件實(shí)現(xiàn)如下:
// 開(kāi)始觸摸時(shí)就會(huì)調(diào)用一次這個(gè)方法 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"摸我干啥!"); } // 手指移動(dòng)就會(huì)調(diào)用這個(gè)方法 // 這個(gè)方法調(diào)用非常頻繁 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"哎呀,不要拽人家!"); } // 手指離開(kāi)屏幕時(shí)就會(huì)調(diào)用一次這個(gè)方法 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"手放開(kāi)還能繼續(xù)玩耍!"); } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"point.x = %f",point.x); NSLog(@"point.y = %f",point.y); NSLog(@"%@",event); return nil; }上述創(chuàng)建的view 對(duì)象點(diǎn)擊 輸出如下
point.x = 70.000000 point.y = 48.666672 <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()} point.x = 70.000000 point.y = 48.666672 <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}因?yàn)椋?code>hitTest:withEvent:方法中返回nil,那么調(diào)用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒(méi)有找到更合適的view。那么最合適的view就是該控件的父控件。
所以事件的傳遞順序是這樣的
產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view變形 - 返回 self.superview
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"point.x = %f",point.x); NSLog(@"point.y = %f",point.y); NSLog(@"%@",event); return self.superview; }輸出與上面的一樣
總結(jié): 即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會(huì)調(diào)用,不然怎么知道有沒(méi)有更合適的!即,如果確定最終父控件是最合適的view,那么該父控件的子控件的hitTest:withEvent:方法也是會(huì)被調(diào)用的。- hitTest:withEvent:方法底層實(shí)現(xiàn)
- (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; // 3.從后往前遍歷子控件數(shù)組 int count = (int)self.subviews.count; for (int i = count - 1; i >= 0; i--) { // 獲取子控件 UIView *childView = self.subviews[i]; // 坐標(biāo)系的轉(zhuǎn)換,把窗口上的點(diǎn)轉(zhuǎn)換為子控件上的點(diǎn) // 把自己控件上的點(diǎn)轉(zhuǎn)換成子控件上的點(diǎn) CGPoint childP = [self convertPoint:point toView:childView]; UIView *fitView = [childView hitTest:childP withEvent:event]; if (fitView) { // 如果能找到最合適的view return fitView; } } // 4.沒(méi)有找到更合適的view,也就是沒(méi)有比自己更合適的view return self; } // 作用:判斷下傳入過(guò)來(lái)的點(diǎn)在不在方法調(diào)用者的坐標(biāo)系上 // point:是方法調(diào)用者坐標(biāo)系上的點(diǎn) //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event //{ // return NO; //}-
pointInside:withEvent:方法
pointInside:withEvent:方法判斷點(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)用者也就不能處理事件
-
-
UIView不能接收觸摸事件的三種情況:
-
不允許交互:userInteractionEnabled = NO -
隱藏:如果把父控件隱藏,那么子控件也會(huì)隱藏,隱藏的控件不能接受事件 -
透明度:如果設(shè)置一個(gè)控件的透明度<0.01,會(huì)直接影響子控件的透明度。alpha:0.0~0.01為透明。
-
-
3.事件的響應(yīng)
-
觸摸事件處理的整體過(guò)程
- 用戶點(diǎn)擊屏幕后產(chǎn)生的一個(gè)觸摸事件,經(jīng)過(guò)一系列的傳遞過(guò)程后,會(huì)找到最合適的視圖控件來(lái)處理這個(gè)事件
- 找到最合適的視圖控件后,就會(huì)調(diào)用控件的touches方法來(lái)作具體的事件處理touchesBegan…touchesMoved…touchedEnded…
- 這些touches方法的默認(rèn)做法是將事件順著響應(yīng)者鏈條向上傳遞(也就是touch方法默認(rèn)不處理事件,只傳遞事件),將事件交給上一個(gè)響應(yīng)者進(jìn)行處理
如何判斷上一個(gè)響應(yīng)者
如果當(dāng)前這個(gè)view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者
如果當(dāng)前這個(gè)view不是控制器的view,那么父控件就是上一個(gè)響應(yīng)者-
響應(yīng)者鏈的事件傳遞過(guò)程:
- 如果當(dāng)前view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者,事件就傳遞給控制器;如果當(dāng)前view不是控制器的view,那么父視圖就是當(dāng)前view的上一個(gè)響應(yīng)者,事件就傳遞給它的父視圖
- 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對(duì)象進(jìn)行處理
- 如果window對(duì)象也不處理,則其將事件或消息傳遞給UIApplication對(duì)象
- 如果UIApplication也不能處理該事件或消息,則將其丟棄
//只要點(diǎn)擊控件,就會(huì)調(diào)用touchBegin,如果沒(méi)有重寫(xiě)這個(gè)方法,自己處理不了觸摸事件 // 上一個(gè)響應(yīng)者可能是父控件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 默認(rèn)會(huì)把事件傳遞給上一個(gè)響應(yīng)者,上一個(gè)響應(yīng)者是父控件,交給父控件處理 [super touchesBegan:touches withEvent:event]; // 注意不是調(diào)用父控件的touches方法,而是調(diào)用父類的touches方法 // super是父類 superview是父控件 } -
總結(jié)
-
事件的傳遞與響應(yīng):
事件的傳遞當(dāng)一個(gè)事件發(fā)生后,事件會(huì)從父控件傳給子控件,也就是說(shuō)由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的傳遞,也就是尋找最合適的view的過(guò)程。事件的響應(yīng)首先看initial view能否處理這個(gè)事件,如果不能則會(huì)將事件傳遞給其上級(jí)視圖(inital view的superView);如果上級(jí)視圖仍然無(wú)法處理則會(huì)繼續(xù)往上傳遞;一直傳遞到視圖控制器view controller,首先判斷視圖控制器的根視圖view是否能處理此事件;如果不能則接著判斷該視圖控制器能否處理此事件,如果還是不能則繼續(xù)向上傳 遞;(對(duì)于第二個(gè)圖視圖控制器本身還在另一個(gè)視圖控制器中,則繼續(xù)交給父視圖控制器的根視圖,如果根視圖不能處理則交給父視圖控制器處理);一直到 window,如果window還是不能處理此事件則繼續(xù)交給application處理,如果最后application還是不能處理此事件則將其丟棄在事件的響應(yīng)中,如果某個(gè)控件實(shí)現(xiàn)了touches...方法,則這個(gè)事件將由該控件來(lái)接受,如果調(diào)用了[supertouches….];就會(huì)將事件順著響應(yīng)者鏈條往上傳遞,傳遞給上一個(gè)響應(yīng)者;接著就會(huì)調(diào)用上一個(gè)響應(yīng)者的touches….方法
-
如何做到一個(gè)事件多個(gè)對(duì)象處理:
因?yàn)橄到y(tǒng)默認(rèn)做法是把事件上拋給父控件,所以可以通過(guò)重寫(xiě)自己的touches方法和父控件的touches方法來(lái)達(dá)到一個(gè)事件多個(gè)對(duì)象處理的目的。- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 1.自己先處理事件... NSLog(@"do somthing..."); // 2.再調(diào)用系統(tǒng)的默認(rèn)做法,再把事件交給上一個(gè)響應(yīng)者處理 [super touchesBegan:touches withEvent:event]; } 事件的傳遞和響應(yīng)的區(qū)別:
事件的傳遞是 從上到下(父控件到子控件)
事件的響應(yīng)是 從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件。