系統響應階段
- 1.手指觸碰屏幕,屏幕感受到觸摸后,將事件交由IOKit來處理。
- 2.IOKIT將觸摸事件封裝成IOHIDEvent對象,并通過mach port傳遞給SpringBoard進程。
mach 是一個卡內基梅隆大學開發的用于支持操作系統研究的操作系統內核。mach是一個真正的內核,是UNIX中BSD的替代內核。
mach port是進程端口,各進程間通過它來通信。Springboard是一個系統進程,可以理解為桌面系統,可以統一管理和分發系統接收到的觸摸事件。
- 3.SpringBoard由于接收到觸摸事件,因此觸發了系統進程的主線程的runloop的source回調
? ???發生觸摸事件的時候,你有可能正在桌面上翻頁,也有可能正在頭條上看新聞,如果是前者,則觸發SpringBoard主線程的runloop的source0回調,將桌面系統交由系統進程去消耗。而如果是后者,則將觸摸事件通過IPC(Inter-Process Communication,進程間通信)傳遞給前臺App進程,后面的事便是App內部對于觸摸事件的響應了。
APP響應觸摸事件
- 1.App進程的mach port接收來自SpringBoard的觸摸事件,主線程的runloop被喚醒,觸發source1回調
- 2.source1回調又觸發了一個source0回調,將接收到的IOHIDEvent對象封裝成UIEvent對象,此時App將正式對于觸摸事件的響應。
- 3.source0回調將觸摸事件添加到UIApplication的事件隊列,當觸摸事件出隊后UIApplication為觸摸事件尋找最佳響應者。
- 4.尋找到最佳響應者之后,接下來的事情便是事件在響應鏈中傳遞和響應。
觸摸 事件 響應者
觸摸
觸摸對象即UITouch對象。
一個手指觸摸屏幕,就會生成一個UITouch對象,如果多個手指同時觸摸,就會生成多個UITouch對象。
多個手指先后觸摸,如果系統判斷多個手指觸摸的是同一個地方,那么不會生成多個UITouch對象,而是更新這個UITouch對象,改變其tap count。如果多個手指觸摸的不是同一個地方,那就會生成多個UITouch對象。
觸摸事件
觸摸事件即UIEvent。
UIEvent即對UITouch的一次封裝。由于一次觸摸事件并不止有一個觸摸對象,可能是多指同時觸摸。觸摸對象集合可以通過allTouches屬性來獲取。
響應者
響應者即UIResponder
下列實例都是UIResponder:
- UIView
- UIViewController
- UIApplication
- Appdelegate
? ???響應者響應觸摸事件是通過下列四個方法來實現的:
//手指觸碰屏幕,觸摸開始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開屏幕,觸摸結束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結束前,某個系統事件中斷了觸摸,例如電話呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
尋找最佳響應者(Hit-Testing)
當App通過mach port得到這個觸摸事件時,App中有那么多UIView或者UIViewController,到底應該給誰去響應呢?尋找最佳響應者就是找出這個優先級最高的響應對象。
尋找最佳響應者的具體流程如下:
1.UIApplication首先將事件傳遞給UIWindow,如果有多個UIWindow對象,則先選擇最后加上的UIWindow對象。
2.若UIWindow對象能響應這個觸摸事件,則繼續向其子視圖傳遞,向子視圖傳遞時也是先傳遞給最后加上的子視圖。
-
3.若子視圖無法響應該事件,則返回父視圖,再傳遞給倒數第二個加入該父視圖的子視圖。
尋找最佳響應者.png
例如上面這張圖,C在B的后面加入,E在F的后面加入。那么尋找最佳響應者的順序就是:
1.UIWindow對象將事件傳遞給視圖A,A判斷自己能否響應觸摸事件,如果能響應,則繼續傳遞給其子視圖。
2.如果A能響應觸摸事件,由于A有兩個子視圖B,C,而C又在B的后面加入的,所以A視圖再把觸摸事件傳遞給C,C再判斷自己能否響應觸摸事件,若能則繼續傳遞給其子視圖,若不能,則A視圖再將觸摸事件傳遞給B視圖。
3.如果C能響應觸摸事件,C視圖也有兩個子視圖,分別是E和F,但是由于E是在F之后加到C上面的,所以先傳遞到,由于E可以響應觸摸事件,所以最終的最佳響應者就是E。
視圖如何判斷自己能否響應觸摸事件?
下列情況下,視圖不能響應觸摸事件:
- 1.觸摸點不在視圖范圍內。
- 2.不允許交互:視圖的userInteractionEnabled = NO。
- 3.隱藏:hidden = YES,如果視圖隱藏了,則不能響應事件。
- 4.透明度:當視圖的透明度小于等于0.01時,不能響應事件。
尋找最佳響應者的原理
hitTest:withEvent:
每個UIView都有一個hitTest:withEvent:方法。這個方法是尋找最佳響應者的核心方法,同時又是傳遞事件的橋梁。它的作用是詢問事件在當前視圖中的響應者。
hitTest:withEvent:返回一個UIView對象,作為當前視圖層次中的響應者。其默認實現是:
- 若當前視圖無法響應事件,則返回nil。
- 若當前視圖能響應事件,但無子視圖可響應事件,則返回當前視圖。
- 若當前視圖能響應事件,同時有子視圖能響應,則返回子視圖層次中的事件響應者。
開始時UIApplication調用UIWindow的hitTest:withEvent:方法將觸摸事件傳遞給UIWindow,如果UIWindow能夠響應觸摸事件,則調用hitTest:withEvent:將事件傳遞給其子視圖并詢問子視圖上的最佳響應者,這樣一級一級傳遞下去,獲取最終的最佳響應者。 hitTest:withEvent:的代碼實現大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3種狀態無法響應事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//觸摸點若不在當前視圖上則無法響應事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//從后往前遍歷子視圖數組 同層次的后加入優先
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 獲取子視圖
UIView *childView = self.subviews[i];
// 坐標系的轉換,把觸摸點在當前視圖上坐標轉換為在子視圖上的坐標
CGPoint childP = [self convertPoint:point toView:childView];
//詢問子視圖層級中的最佳響應視圖
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子視圖中有更合適的就返回
return fitView;
}
}
//沒有在子視圖中找到更合適的響應視圖,那么自身就是最合適的
return self;
}
注意這里的方法pointInside:withEvent:,這個方法是判斷觸摸點是否在視圖范圍內。默認的實現是如果觸摸點在視圖范圍內則返回YES,否則返回NO。
下面我們在上圖中的每個視圖層次中添加三個方法來驗證之前的分析:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
點擊視圖,打印出來的結果是:
-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[EView touchesBegan:withEvent:]
這和我們的分析是一致的。
大家看一下上面的圖,其中A和B都是根視圖控制器的View的子視圖,C是加在B上的子視圖。當我們觸摸C中在A的那部分的視圖的時候,我們打印看看:
2018-04-13 19:37:19.985968+0800 UITouchDemo[9174:387327] -[BView hitTest:withEvent:]
2018-04-13 19:37:19.987782+0800 UITouchDemo[9174:387327] -[BView pointInside:withEvent:]
2018-04-13 19:37:19.988017+0800 UITouchDemo[9174:387327] -[AView hitTest:withEvent:]
2018-04-13 19:37:19.988294+0800 UITouchDemo[9174:387327] -[AView pointInside:withEvent:]
2018-04-13 19:37:19.990704+0800 UITouchDemo[9174:387327] -[AView touchesBegan:withEvent:]
通過打印結果我們發現,觸摸事件壓根就沒有傳遞到C視圖這里,這是為什么呢?
原來,觸摸事件最早傳遞到B視圖,然后調用B視圖的hitTest:withEvent:方法,在這個方法中會調用pointInside:withEvent:
來斷觸摸點是否在視圖范圍內,這里由于觸摸的點是在A視圖的那部分,所以不在B視圖的那部分,因此返回NO。這樣觸摸事件就傳遞到了A視圖,由于A可以響應觸摸事件,而A又沒有子視圖,所以最終的最佳響應者就是A視圖。
那么這顯然不是我們希望看到的,我們希望的是當觸摸C時,不管觸摸的是C的哪里,C都能成為最佳響應者響應觸摸事件。
要解決這個問題也很容易,我們只需要在B視圖中重寫pointInside:withEvent:
方法。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s", __func__);
CGPoint tmpPoint = [self convertPoint:point toView:_cView];
if([_cView pointInside:tmpPoint withEvent:event]){
return YES;
}
return [super pointInside:point withEvent:event];
}
我們判斷觸摸點位置是否在視圖C范圍內,如果在視圖C的范圍內,則直接返回YES。
觸摸事件的響應
通過hitTest:withEvent:
我們已經找到了最佳響應者,下面要做的事情就是讓這個最佳響應者響應觸摸事件。這個最佳響應者對于觸摸事件擁有決定權,它可以決定是自己一個響應這個事件,也可以自己響應之后還把它傳遞給其他響應者。這個由響應者構成的就是響應鏈。
響應者對于事件的響應和傳遞都是在touchesBegan:withEvent:
這個方法中完成的。該方法默認的實現是將該方法沿著響應鏈往下傳遞
響應者對于接收到的事件有三種操作:
- 1.默認的操作。不攔截,事件會沿著默認的響應鏈自動往下傳遞。
- 2.攔截,不再往下分發事件,重寫
touchesBegan:withEvent:
方法,不調用父類的touchesBegan:withEvent:
方法。 - 3.不攔截,繼續往下分發事件,重新
touchesBegan:withEvent:
方法,并調用父類的touchesBegan:withEvent:
方法。
我們一般在編寫代碼時,如果某個視圖響應事件,會在該視圖類中重寫touchesBegan:withEvent:
方法,但是并不會調用父類的touchesBegan:withEvent:
方法,這樣我們就把這個事件攔截下來了,不再沿著響應鏈往下傳遞。那么我們為什么想要沿著響應鏈傳遞事件就要重寫父類的touchesBegan:withEvent:
方法呢?因為父類的touchesBegan:withEvent:
方法默認是向下傳遞的。我們重寫touchesBegan:withEvent:
并調用父類的方法就是既對觸摸事件實現了響應,又將事件沿著響應鏈傳遞了。
響應鏈中的事件傳遞規則
每一個響應者對象都有一個nextResponder
方法,用來獲取響應鏈中當前響應者對象的下一個響應者。因此,如果事件的最佳響應者確定了,那么整個響應鏈也就確定了。
對于響應者對象,默認的nextResponder
對象如下:
-
UIView
若視圖是UIViewController的view。則其nextResponder
是UIViewController,若其只是單獨的視圖,則其nextResponder
是其父視圖。 -
UIViewController
若該viewController是window的rootViewController,則其nextResponder
為窗口對象,若其是由其他視圖控制器present的,則其nextResponder
是presenting View Controller。 - UIWindow
nextResponder
為UIApplication對象。
上圖是官網對于響應鏈的示例展示,如果最佳響應者對象是UITextField,則響應鏈為:
- UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate.
現在我們可以猜想,在父類的touchesBegan:withEvent:方法中,可能調用了[self.nextResponder touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event]這樣來將事件沿著響應鏈傳遞。
UIResponder、UIGestureRecognizer、UIControl的優先級
不光UIResponder能響應觸摸事件,UIGestureRecognizer和UIControl也能處理觸摸事件。
UIGestureRecognizer
我們首先來看一個場景
我們給上圖中的黃色視圖A添加tap事件:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
[tap addTarget:self action:@selector(tapGesture)];
[self addGestureRecognizer:tap];
添加點擊事件:
- (void)tapGesture{
NSLog(@"taped");
}
運行程序,點擊黃色視圖A,看打印結果:
2018-04-15 16:36:25.378952+0800 UITouchDemo[14824:351042] -[AView touchesBegan:withEvent:]
2018-04-15 16:36:25.388247+0800 UITouchDemo[14824:351042] taped
2018-04-15 16:36:25.391769+0800 UITouchDemo[14824:351042] -[AView touchesCancelled:withEvent:]
首先響應者A調用了touchesBegan:withEvent:
響應了tap。然后執行了手勢識別器的函數,最后touchesCancelled:withEvent:
函數確被調用,正確的應該是最后touchesEnded:withEvent:
函數被調用,這是怎么回事呢?Apple的解釋是:
window在將事件傳遞給最佳響應者之前會把事件先傳給手勢識別器,然后再傳給最佳響應者,當手勢識別器已經識別了手勢,最佳響應者對象會調用
touchesCancelled:withEvent:
方法終止對事件的響應。
如果按照這個理論,上面的結果也應該是先打印taped
后打印-[AView touchesBegan:withEvent:]
呀,為什么不是這樣呢?問題出在,打印taped
并不代表是這個時候事件傳遞到了手勢識別器這里,而是手勢識別器這個時候正式識別了手勢。
正式識別了這個手勢和事件被傳遞到了手勢識別器這里的時間是不一樣的。
那么我們怎樣才能知道事件是先傳遞給了最佳響應者還是壽司識別器呢?只需要找到手勢識別器的響應函數然后打印它們即可。手勢識別器的響應函數和UIResponder的響應函數非常相似:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
我們重寫了一個單擊手勢類,繼承自UITapGestureRecognizer。在這個類里導入頭文件<UIKit/UIGestureRecognizerSubclass.h>:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s,%s",object_getClassName(self.view), __func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s,%s",object_getClassName(self.view), __func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s,%s",object_getClassName(self.view), __func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s,%s",object_getClassName(self.view), __func__);
[super touchesCancelled:touches withEvent:event];
}
這樣我們就可以打印手勢識別器接收事件的時間。我們打印結果:
2018-04-16 14:53:20.444618+0800 UITouchDemo[24410:731610] AView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 14:53:20.451872+0800 UITouchDemo[24410:731610] -[AView touchesBegan:withEvent:]
2018-04-16 14:53:20.452245+0800 UITouchDemo[24410:731610] AView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 14:53:20.455192+0800 UITouchDemo[24410:731610] AView taped
2018-04-16 14:53:20.455448+0800 UITouchDemo[24410:731610] -[AView touchesCancelled:withEvent:]
通過打印結果我們能夠很清楚的看到,事件最先傳給了手勢識別器,然后傳遞給了最佳響應者,在手勢識別器識別成功手勢后,調用最佳響應者的touchesCancelled:
方法終止了最佳響應者對于事件的響應。
下面再看一個情景:
在上圖中,視圖A,B,C上都添加了手勢識別器,那么當我們單擊C視圖的時候,事件是一個怎么樣的響應過程呢?我們打印結果看一下:
2018-04-16 15:03:21.809456+0800 UITouchDemo[24654:740042] AView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:03:21.811451+0800 UITouchDemo[24654:740042] UIView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:03:21.813232+0800 UITouchDemo[24654:740042] CView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:03:21.815768+0800 UITouchDemo[24654:740042] BView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:03:21.818022+0800 UITouchDemo[24654:740042] -[CView touchesBegan:withEvent:]
2018-04-16 15:03:21.818708+0800 UITouchDemo[24654:740042] AView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:03:21.818899+0800 UITouchDemo[24654:740042] UIView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:03:21.819147+0800 UITouchDemo[24654:740042] CView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:03:21.819552+0800 UITouchDemo[24654:740042] BView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:03:21.820637+0800 UITouchDemo[24654:740042] CView taped
2018-04-16 15:03:21.820967+0800 UITouchDemo[24654:740042] -[CView touchesCancelled:withEvent:]
我們可以看到,事件首先傳遞給了A,UIView,B,C這幾個視圖上面的手勢識別器,然后才傳遞給了最佳響應者C視圖,A,UIView,B,C這幾個視圖的手勢識別器都識別了手勢之后,調用最佳響應者的touchesCancelled:withEvent:
方法來取消最佳響應者對于事件的響應。
再來運行一下程序,打印執行結果:
2018-04-16 15:09:53.877158+0800 UITouchDemo[24765:744167] UIView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:09:53.877720+0800 UITouchDemo[24765:744167] AView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:09:53.878351+0800 UITouchDemo[24765:744167] CView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:09:53.878720+0800 UITouchDemo[24765:744167] BView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 15:09:53.880317+0800 UITouchDemo[24765:744167] -[CView touchesBegan:withEvent:]
2018-04-16 15:09:53.886045+0800 UITouchDemo[24765:744167] UIView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:09:53.887088+0800 UITouchDemo[24765:744167] AView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:09:53.887661+0800 UITouchDemo[24765:744167] CView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:09:53.888026+0800 UITouchDemo[24765:744167] BView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 15:09:53.888661+0800 UITouchDemo[24765:744167] CView taped
2018-04-16 15:09:53.889124+0800 UITouchDemo[24765:744167] -[CView touchesCancelled:withEvent:]
我們看到,UIView,A,B,C這四個視圖上的手勢識別器接收事件的順序發生了變化,但是最佳響應者CView一定是最后接收事件的,并且最后響應的函數一定是CView上綁定的手勢識別器的函數。由此我們得出結論:
當響應鏈上有手勢識別器時,事件在傳遞過程中一定會先傳遞給響應鏈上的手勢識別器,然后才傳遞給最佳響應者,當響應鏈上的手勢識別了手勢后就會取消最佳響應者對于事件的響應。事件傳遞給響應鏈上的手勢識別器時是亂序的,并不是按照響應鏈從頂至底傳遞,但是最后響應的函數還是響應鏈最頂端的手勢識別器函數。
手勢識別器的三個屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
先總結一下手勢識別器和UIResponder對于事件響應的聯系:
- Window先將事件傳遞給響應鏈上的手勢識別器,再傳遞給UIResponder。
- 手勢識別器識別手勢期間,如果觸摸對象的狀態發生變化,都是先發送給手勢識別器,再發送給UIResponder。
- 若手勢識別器已經成功識別了手勢,則停止UIResponder對于事件的響應,并停止向UIResponder發送事件。
- 若手勢識別器未能識別手勢,而此時觸摸未結束,則停止向手勢識別器發送手勢,僅向UIResponder發送事件。
- 若手勢識別器未能識別手勢,而此時觸摸已經結束,則向UIResponder發送end狀態的touch事件以停止對事件的響應。
- 1、cancelsTouchesInView
默認為yes。表示當手勢識別成功后,取消最佳響應者對象對于事件的響應,并不再向最佳響應者發送事件。若設置為No,則表示在手勢識別器識別成功后仍然向最佳響應者發送事件,最佳響應者仍響應事件,會調用touchesEnded:withEvent:
- 2、delaysTouchesBegan
默認為No,即在手勢識別器識別手勢期間,觸摸對象狀態發生變化時,都會發送給最佳響應者,調用touchesBegan:withEvent:
,若設置成yes,則在識別手勢期間,觸摸狀態發生變化時不會發送給最佳響應者,不會調用touchesBegan:withEvent:
- 3.delaysTouchesEnded
默認為NO。默認情況下當手勢識別器未能識別手勢時,若此時觸摸已經結束,則會立即通知Application發送狀態為end的touch事件給最佳響應者以調用 touchesEnded:withEvent: 結束事件響應;若設置為YES,則會在手勢識別失敗時,延遲一小段時間(0.15s)再調用響應者的 touchesEnded:withEvent:。
UIControl
UIControl是系統提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子類。當UIControl跟蹤到觸摸事件時,會向其上添加的target發送事件以執行action。值得注意的是,UIConotrol是UIView的子類,因此本身也具備UIResponder應有的身份。
看下面一種情景
圖中視圖A,B,C上都添加有單擊手勢,C上面的黑色按鈕添加有action。當我們點擊C上面的黑色按鈕時,看打印結果:
2018-04-16 16:05:35.555304+0800 UITouchDemo[25754:780177] CView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.555745+0800 UITouchDemo[25754:780177] BView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.556011+0800 UITouchDemo[25754:780177] AView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.556573+0800 UITouchDemo[25754:780177] UIButton,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.559354+0800 UITouchDemo[25754:780177] CView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.559600+0800 UITouchDemo[25754:780177] BView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.560494+0800 UITouchDemo[25754:780177] AView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.561018+0800 UITouchDemo[25754:780177] UIButton,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.562089+0800 UITouchDemo[25754:780177] Button taped
我們看到,雖然事件都傳遞給了響應鏈上的手勢識別器,但是這些手勢識別器綁定的函數最后都沒有響應,而是響應的黑色按鈕綁定的action。我們再在黑色按鈕上面加一個單擊手勢,然后單擊黑色按鈕,看打印結果:
2018-04-16 16:05:35.555304+0800 UITouchDemo[25754:780177] CView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.555745+0800 UITouchDemo[25754:780177] BView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.556011+0800 UITouchDemo[25754:780177] AView,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.556573+0800 UITouchDemo[25754:780177] UIButton,-[PDTapGestureRecognizer touchesBegan:withEvent:]
2018-04-16 16:05:35.559354+0800 UITouchDemo[25754:780177] CView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.559600+0800 UITouchDemo[25754:780177] BView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.560494+0800 UITouchDemo[25754:780177] AView,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.561018+0800 UITouchDemo[25754:780177] UIButton,-[PDTapGestureRecognizer touchesEnded:withEvent:]
2018-04-16 16:05:35.562089+0800 UITouchDemo[25754:780177] Button taped
可以看到,當UIControl上面添加了手勢后,UIControl不會響應自己的action。
因此得出結論:
UIControl會阻止父視圖上的手勢識別器的行為,也就是UIControl的執行優先級比父視圖上面的UIGestureRecognizer要高,但是比UIControl自身的UIGestureRecognizer優先級要低。
總結
系統觸摸事件響應鏈如下:
- 事件觸發:主屏幕觸摸事件 --> 系統進程 --> App進程接收系統觸摸事件,觸發source1回調 --> App進程觸發source0回調 --> source0回調將觸摸事件添加到Application事件隊列
-
尋找最佳響應者:首先通過
hitTest:withEvent:
尋找最佳響應者,順序為:UIApplication --> UIWindow --> UIViewController --> UIViewController.view --> view -
響應事件:再者通過
touchesBegan:withEvent:
來響應和傳遞這個觸摸事件。默認順序為:最佳響應者 --> 最佳響應者的父View --> UIViewController.view --> UIViewController --> UIWindow --> UIApplication --> UIApplicationDelegate - 觸摸事件優先級:UIGestureRecognizer > UIControl > 父view的UIGestureRecognizer > UIResponder
轉載參考自:iOS中觸摸事件傳遞和響應原理
Reference