@[TOC](IOS 事件般又,響應鏈機制分析)
1. 事件分發(fā)和響應者鏈條
1.1 簡述
- 事件分發(fā):自上而下的由UIApplication開始,一路往最具體的View查找丐黄,直到找到最應該處理并且能夠處理事件的那個控件斋配。
- 響應者鏈條:當找到那個最應該處理并且能夠處理事件的那個控件以后,如果這個控件確實處理了這個事件灌闺,那么這個事件的就到此處理完畢艰争,但是很有可能出現(xiàn)的情況是,雖然這個控件最應該處理桂对,也能夠處理事件甩卓,但是它并沒有處理事件,那么這時這個事件就要傳給下一個響應者處理蕉斜,下一個響應者還不處理逾柿,那就再下一個,這個事件就沿著這條響應者鏈條找直到響應者確實處理了這個事件(并中斷了事件傳遞宅此,就是在touch方法里沒有調(diào)用下一個響應者的touch方法)為止
事件的分發(fā)路徑和響應者鏈條的路徑并不是同一條路從兩頭走的關系机错。首先,事件分發(fā)時候除了最開始的UIApplication诽凌,一路都是在查找下一個更應該響應的UIView毡熏,而響應者鏈條除了考慮UIView以外還有其他的UIResponder,例如UIViewController坦敌;其次侣诵,事件分發(fā)會考慮同級UIView之間的關系,就是如果一個UIView有多個子View狱窘,那么哪個是更應該響應的View杜顺,而響應者鏈條則不會考慮同級View之間的關系,一個View的下一個響應者并不會考慮除了自己以為其它適合響應的同級View蘸炸,而是之間考慮它的父View或者是控制器躬络。
2. 事件分發(fā)
當我們手指觸摸屏幕時,系統(tǒng)會生成UITouch對象(一個手指一個UITouch)搭儒,然后系統(tǒng)又會幫我們把所有的UITouch對象包裝成一個UIEvent穷当,然后把這個UIEvent交給UIApplication單例維護著的一個事件隊列提茁,當輪到這個事件處理的時候,UIApplication首先會將這個UIEvent交給KeyWindow馁菜,然后KeyWindow再交給它的根控制器的View茴扁,然后不斷的遞歸尋找最適合處理這個事件的View。
這個遞歸查找的方法叫做hitTest
下面我們嘗試的寫一下這個方法的實現(xiàn):
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//如果不能處理,返回nil
if (self.hidden==YES||self.alpha<=0.01||self.userInteractionEnabled == NO) {
return nil;
}
//如果不應該處理返回nil
if ([self pointInside:point withEvent:event]==NO) {
return nil;
}
//找找看有沒有更適合處理的子View
//最后添加的(也就是最上面的View)先處理
NSArray *reverseArray = [[self.subviews reverseObjectEnumerator]allObjects];
for (UIView *subView in reverseArray) {
CGPoint subPoint = [self convertPoint:point toView:subView];
UIView *resultView = [subView hitTest:subPoint withEvent:event];
if (resultView) {
return resultView;
}
}
//遍歷完都沒有子控件更適合,那么最適合的就是自己
return self;
}
3. 響應者鏈條
當找到這個最適合的控件后汪疮,會從這個控件開始嘗試處理事件并傳遞給下一個響應者(直到某個響應者中斷了傳遞)峭火。
那么,怎么尋找下一個響應者呢智嚷?原則如下:
如果這個view是一個控制器的View那么這個View的下一個響應者就是控制器卖丸。
如果這個View不是控制器的View那么下一個響應者就是它的父View。
根控制器的下一個響應者是誰UIWindow盏道,再下一個響應者是UIApplication稍浆,鏈條結束。
如果我們把A控制器的View添加到B控制器的View上猜嘱,那么A控制器會在響應者鏈條上嗎粹湃,如果在,它的下一個響應者是誰泉坐?
如果要把A控制器添加到響應者鏈條为鳄,就要B控制器add A為子控制器,如果不add的話A控制器不會在這個響應者鏈條內(nèi)腕让,A控制器的下一個響應者為A控制器View的父View孤钦。
- 響應鏈是如何工作,正確找到應該響應該事件的響應者的纯丸?
UIKit使用基于視圖的hit-testing來確定touch事件發(fā)生的位置偏形。具體解釋就是,UIKit將touch的位置和視圖層級中的view的邊界進行了比較觉鼻,UIView的方法 hitTest:withEvent: 在視圖層級中進行俊扭,尋找包含指定touch的最深子視圖。這個視圖成為touch事件的第一個響應者坠陈。
說白了就是萨惑,當有touch事件來的時候,會從最下面的視圖開始執(zhí)行 hitTest:withEvent: 仇矾,如果符合成為響應者的條件庸蔼,就會繼續(xù)遍歷它的 subviews 繼續(xù)執(zhí)行 hitTest:withEvent: ,直到找到最合適的view成為響應者贮匕。
- 符合響應者的條件包括:
- touch事件的位置在響應者區(qū)域內(nèi)
- 響應者 hidden 屬性不為 YES
- 響應者 透明度 不是 0
- 響應者 userInteractionEnabled 不為 NO
- 響應者鏈有以下特點:
1姐仅、響應者鏈通常是由視圖(UIView)構成的;
2、一個視圖的下一個響應者是它視圖控制器(UIViewController)(如果有的話)掏膏,然后再轉(zhuǎn)給它的父視圖(Super View)劳翰;
3、視圖控制器(如果有的話)的下一個響應者為其管理的視圖的父視圖馒疹;
4磕道、單例的窗口(UIWindow)的內(nèi)容視圖將指向窗口本身作為它的下一個響應者
需要指出的是,Cocoa Touch應用不像Cocoa應用行冰,它只有一個UIWindow對象溺蕉,因此整個響應者鏈要簡單一點;
4. 事件傳遞和響應原理分析
4.1事件傳遞流程圖
-
事件流程圖
在這里插入圖片描述
IOKit.framework 為系統(tǒng)內(nèi)核的庫
SpringBoard.app 相當于手機的桌面
Source1 主要接收系統(tǒng)的消息
Source0 - UIApplication - UIWindow
- 從UIWindow 開始步驟悼做,見下圖
比如我們在self.view 上依次添加view1疯特、view2、view3(3個view是同級關系)肛走,那么系統(tǒng)用hitTest以及pointInside時會先從view3開始便利漓雅,如果pointInside返回YES就繼續(xù)遍歷view3的subviews(如果view3沒有子視圖,那么會返回view3)朽色,如果pointInside返回NO就開始便利view2邻吞。
反序遍歷,最后一個添加的subview開始葫男。也算是一種算法優(yōu)化
4.2 HitTest 抱冷、pointInside
當我們觸控手機屏幕時系統(tǒng)便會將這一操作封裝成一個UIEvent放到事件隊列里面,然后Application從事件隊列取出這個事件梢褐,接著需要找到去響應這個事件的最佳視圖也就是Responder, 所以開始的第一步應該是找到Responder, 那么又是如何找到的呢旺遮?那就不得不引出UIView的2個方法:
- -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
返回視圖層級中能響應觸控點的最深視圖 - -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
返回視圖是否包含指定的某個點
- 通過簡單示例代碼來驗證一下
EOCLightGrayView *grayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
redView = [[EOCRedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
EOCBlueView *blueView = [[EOCBlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
EOCYellowView *yellowView = [[EOCYellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
[self.view addSubview:grayView];
[grayView addSubview:redView];
[grayView addSubview:blueView];
[self.view addSubview:yellowView];
測試結果如下:
我們可以得出結論:
點擊red,由于yellow 與 grey 同級盈咳,yellow 比 grey 后添加耿眉,所以先打印yellow,由于觸摸點不在yellow內(nèi)鱼响,打印grey鸣剪,然后遍歷grey,打印他的兩個subviews
通過在HitTest返回nil丈积,pointInside并沒有執(zhí)行筐骇,我們可以得知,pointInside調(diào)用順序你在HitTest之后的桶癣。
pointInside 的 參數(shù) :(CGPoint)poinit 的值是以自身為坐標系的拥褂,判斷點是否view內(nèi)的范圍是以view自身的bounds為范圍,而非frame
如果在grey的hitTest返回[super hitTest:point event:event]牙寞,則會執(zhí)行gery.subviews的遍歷(subviews 的 hitTest 與 pointInside),grey 的 pointInside 是判斷觸摸點是否在grey的bounds內(nèi)(不準確),grey 的 hitTest 是判斷是否需要遍歷他的subviews.
pointInside 只是在執(zhí)行hitTest時间雀,會在hitTest內(nèi)部調(diào)用的一個方法
pointInside 只是輔助hitTest的關系
hitTest是一個遞歸函數(shù)
- 還原h(huán)itTest內(nèi)部實現(xiàn)代碼
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
///hitTest:判斷pointInside悔详,是不是在view里?是的話惹挟,遍歷茄螃,不是的話返回nil;假設我就是點擊灰色的,返回的是自己连锯;
NSLog(@"%s",__func__);
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
UIView *tmpView = nil;
for (UIView *view in subViews) {
CGPoint convertedPoint = [self convertPoint:point toView:view];
if ([view pointInside:convertedPoint withEvent:event]) {
tmpView = view;
break;
}
}
if (tmpView) {
return tmpView;
} else if([self pointInside:point withEvent:event]) {
return self;
} else {
return nil;
}
return [self hitTest:point event:event]; //redView
///這里是hitTest的邏輯
///alpha(<=0.01)归苍、userInterActionEnabled(NO)、hidden(YES) pointInside返回的為NO
}
4.3 UIRespond 與 響應鏈的組成
所有視圖按照樹狀層次結構組織,每個view都有自己的superView, 包括vc的self.view:
當一個view被添加到superView上的時候, 它的nextResponder就會被指向所在controller
當vc被初始化的時候,self.view的nextResponder會被指向所在的controller
如果當前這個view是控制器的self.view,那么控制器就是上一個響應者,如果當前這個view不是控制器的view,那么父控件就是上一個響應者)vc的nextResponder會被指向self.view的superView
最頂級的vc的nextResponder指向UIWindow
UIWindow的nextResponder指向UIApplication
這就形成了響應鏈,通過UIResponder串連起來的
touches方法實際上沒做什么,UIView繼承了它并重寫,把事件傳遞給nextResponder,相當于[self.nextResponder touchBegan:touches withEvent:event]. 當一個view沒有重寫touch事件,那么這個事件就會一直傳遞下去, 直到UIApplication. 如果重寫了touch方法,這個view響應了事件,事件就被攔截了, 它的nextResponder不會收到這個事件.
- 響應鏈事件傳遞 向上傳遞:
如果view的控制器存在, 就傳遞給控制器,如果控制器不存在,則將其傳遞給它的父視圖.
在視圖層次結構的最頂級視圖,如果不能處理收到的事件/消息,則將事件/消息傳遞給window對象進行處理
如果window對象不處理,則將其事件/消息傳遞給UIApplication對象
如果UIApplication不處理事件/消息,則將其丟棄
- 監(jiān)聽事件的基本流程:
當應用程序啟動以后創(chuàng)建UIApplication對象
然后啟動消息循環(huán)監(jiān)聽所有事件
當用戶觸摸屏幕的時候,消息循環(huán)監(jiān)聽到這個觸摸事件
消息循環(huán)首先把監(jiān)聽到的觸摸事件傳遞給UIApplication對象
UIApplication對象再傳遞給UIWindow對象
UIWindow對象再傳遞給UIWindow的根控制器rootViewController
控制器再傳遞給控制器所管理的view
控制器所管理的view在其內(nèi)部搜索看本次觸摸的點在哪個控件的范圍內(nèi)
找到某個控件以后,調(diào)用這個控件的touchBegan方法,再一次向上返回,最終返回給消息循環(huán)
消息循環(huán)知道哪個按鈕被點擊后, 在搜索這個按鈕是否注冊了對應的事件,如果注冊了,就調(diào)用這個事件處理程序.(一般就是執(zhí)行控制器中的事件處理方法)
4.4 手勢與事件關系
- 手勢 與 hitTest 的關系
相同上面的學習我們可以推測出运怖,手勢的響應也得必須經(jīng)過hitTest先找到視圖才能觸發(fā)(已驗證)
- 手勢與 觸摸事件的關系
touch事件是UIView內(nèi)部的東西拼弃,而手勢疊加上去的觸摸事件
subview會響應superview的手勢, 但是同級的subview不會響應
- 系統(tǒng)如何分辨手勢種類
首先我們想在手勢中調(diào)用 touches 方法必須要導入
import <UIKit/UIGestureRecognizerSubclass.h>
因為gesture繼承的是NSObject 而不是 UIRespond
通過嘗試不調(diào)用 tap手勢 的touchesBegan 摇展,發(fā)現(xiàn)tap手勢無法響應
通過嘗試調(diào)用touchesBegan 吻氧,但是不調(diào)用 pan手勢 的touchesMoved ,發(fā)現(xiàn)pan手勢無法響應
我們通過UITouch的實例咏连,可以看到里面有很多屬性盯孙,比如點擊的次數(shù),上次的位置等祟滴,結合這個屬性系統(tǒng)與touches方法就可以判斷出你使用的是什么手勢
- 手勢與view的touches事件的關系
首先通過觸摸事件振惰,先響應touchesBegan 以及 touchesMoved,直到手勢被識別出來垄懂,調(diào)用touchesCancelled报账,全權交給手勢處理。
但是我們可以改變這種關系
下面是系統(tǒng)的默認設置
tapGesture.delaysTouchesBegan = NO;
///是否延遲view的touch事件識別埠偿;如果延遲了(YES)透罢,并且手勢也識別到了,touch事件會被拋棄
tapGesture.cancelsTouchesInView = YES;
///識別手勢之后冠蒋,是否取消view的touch事件
// 如果為NO, touchesCancelled 不會調(diào)用羽圃,取而代之的是手勢結束后touchesEnd
4.5 手勢識別
手勢識別器 UIGestureRecognizer
簡介
UIGestureRecognizer是蘋果在iOS 3.2之后,推出的手勢識別功能抖剿。UIGestureRecognizer是一個抽象類朽寞,將觸摸事件封裝成了手勢對象,大大簡化了開發(fā)者的開發(fā)難度斩郎,同時也提升了用戶的交互體驗脑融。UIGestureRecognizer有七個子類,它們具體實現(xiàn)了不同手勢的功能缩宜。
屬性方法肘迎,代理
- 初始化甥温、添加target、移除target
//初始化方法 且 添加 target的方法
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action
//單獨添加target的方法
- (void)addTarget:(id)target action:(SEL)action;
//移除target的方法
- (void)removeTarget:(nullable id)target action:(nullable SEL)action;
- 屬性和方法
//手勢的狀態(tài)
@property(nonatomic,readonly) UIGestureRecognizerState state;
//手勢代理
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate;
//手勢是否有效 默認YES
@property(nonatomic, getter=isEnabled) BOOL enabled;
//獲取手勢所在的view
@property(nullable, nonatomic,readonly) UIView *view;
//取消view上面的touch事件響應 default YES **下面會詳解該屬性**
@property(nonatomic) BOOL cancelsTouchesInView;
//延遲touch事件開始 default NO **下面會詳解該屬性**
@property(nonatomic) BOOL delaysTouchesBegan;
//延遲touch事件結束 default YES **下面會詳解該屬性**
@property(nonatomic) BOOL delaysTouchesEnded;
//允許touch的類型數(shù)組妓布,**下面會詳解該屬性**
@property(nonatomic, copy) NSArray<NSNumber *> *allowedTouchTypes
//允許按壓press的類型數(shù)組
@property(nonatomic, copy) NSArray<NSNumber *> *allowedPressTypes
//是否只允許一種touchType 類型姻蚓,**下面會詳解該屬性**
@property (nonatomic) BOOL requiresExclusiveTouchType
//手勢依賴(手勢互斥)方法,**下面會詳解該方法**
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
//獲取在傳入view的點擊位置的信息方法
- (CGPoint)locationInView:(nullable UIView*)view;
//獲取觸摸點數(shù)
@property(nonatomic, readonly) NSUInteger numberOfTouches;
//(touchIndex 是第幾個觸摸點)用來獲取多觸摸點在view上位置信息的方法
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view;
// 給手勢加一個名字匣沼,以方便調(diào)式(iOS11 or later可以用)
@property (nullable, nonatomic, copy) NSString *name API_AVAILABLE(ios(11.0)
先來說說requiresExclusiveTouchType這個屬性
是不是有很多人和我之前一樣狰挡,把它理解成了設置為NO,就可以同時響應幾種手勢點擊了呢释涛?
這個屬性的意思:是否同時只接受一種觸摸類型加叁,而不是是否同時只接受一種手勢。默認是YES唇撬。設置成NO它匕,它會同時響應 allowedTouchTypes 這個數(shù)組里的所有觸摸類型。這個數(shù)組里面裝的touchType類型如下:
//目前touchType有三種
typedef NS_ENUM(NSInteger, UITouchType) {
UITouchTypeDirect, // 手指直接接觸屏幕
UITouchTypeIndirect, // 不是手指直接接觸屏幕(例如:蘋果TV遙控設置屏幕上的按鈕)
UITouchTypeStylus NS_AVAILABLE_IOS(9_1), // 觸控筆接觸屏幕
}
如果把requiresExclusiveTouchType設置為NO局荚,假設view上添加了tapGesture手勢超凳,你同時用手點擊和用觸控筆點擊該view,這個tapGesture手勢的方法都會響應耀态。
接下來說說cancelsTouchesInView轮傍、delaysTouchesBegan、delaysTouchesEnd這三個屬性首装。
cancelsTouchesInView 屬性默認設置為YES创夜,如果識別到了手勢,系統(tǒng)將會發(fā)送touchesCancelled:withEvent:消息仙逻,終止觸摸事件的傳遞驰吓。也就是說默認當識別到手勢時,touch事件傳遞的方法將被終止系奉,如果設置為NO檬贰,touch事件傳遞的方法仍然會被執(zhí)行。
delaysTouchesBegan 用于控制事件的開始響應的時機缺亮,"是否延遲響應觸摸事件"翁涤。設置為NO,不會延遲響應觸摸事件萌踱,如果我們設置為YES葵礼,在手勢沒有被識別失敗前,都不會給事件傳遞鏈發(fā)送消息并鸵。
delaysTouchesEnd 用于控制事件結束響應的時機鸳粉,"是否延遲結束觸摸事件",設置為NO园担,則會立馬調(diào)用touchEnd:withEvent這個方法(如果需要調(diào)用的話)届谈。設置為YES枯夜,會等待一個很短的時間,如果沒有接收到新的手勢識別任務疼约,才會發(fā)送touchesEnded消息到事件傳遞鏈卤档。
手勢依賴方法-requireGestureRecognizerToFail
用法:[A requireGestureRecognizerToFail:B] 當A蝙泼、B兩個手勢同時滿足響應手勢方法的條件時程剥,B優(yōu)先響應,A不響應汤踏。如果B不滿足條件织鲸,A滿足響應手勢方法的條件,則A響應溪胶。其實這就是一個設置響應手勢優(yōu)先級的方法搂擦。
如果一個view上添加了多個手勢對象的,默認這些手勢是互斥的哗脖,一個手勢觸發(fā)了就會默認屏蔽其他手勢動作瀑踢。比如,單擊和雙擊手勢并存時才避,如果不做處理橱夭,它就只能發(fā)送出單擊的消息。為了能夠優(yōu)先識別雙擊手勢桑逝,我們就可以用requireGestureRecognizerToFail:這個方法設置優(yōu)先響應雙擊手勢棘劣。
- UIGestureRecognizerDelegate代理方法
//開始進行手勢識別時調(diào)用的方法,返回NO楞遏,則手勢識別失敗
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
//手指觸摸屏幕后回調(diào)的方法茬暇,返回NO則手勢識別失敗
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch;
//是否支持同時多個手勢觸發(fā)
//返回YES,則可以多個手勢一起觸發(fā)方法寡喝,返回NO則為互斥
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)
otherGestureRecognizer;
//下面這個兩個方法也是用來控制手勢的互斥執(zhí)行的
//這個方法返回YES糙俗,第二個手勢的優(yōu)先級高于第一個手勢
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)
otherGestureRecognizer
//這個方法返回YES,第一個手勢的優(yōu)先級高于第二個手勢
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)
otherGestureRecognizer
子類
點擊手勢——UITapGestureRecognizer
捏合手勢——UIPinchGestureRecognizer
旋轉(zhuǎn)手勢——UIRotationGestureRecognizer
滑動手勢——UISwipeGestureRecognizer
長按手勢——UILongPressGestureRecognizer
平移手勢——UIPanGestureRecognzer
屏幕邊緣平移手勢——UIScreenEdgePanGestureRecognzer