IOS中触摸事件学习
IOS中觸摸事件學習
- 1. 事件的聲明周期
- 2. 系統相應階段
- 3. APP響應階段
- 4. 觸摸、事件、響應者
- 4.1 UITouch(觸摸)
- 4.2 UIEvent(事件真身)
- 4.3 UIResponder(響應者)
- 5. 尋找事件的最佳響應者(Hit-Testing)
- 5.1 事件自下而上的傳遞
- 5.2 Hit-Testing的本質
- 5.3 Hit-Testing過程中的事件攔截(自定義事件流向)
- 5.4 事件的響應以及在響應鏈中的傳遞
- 5.4.1 事件響應的前奏
- 5.4.2 事件的響應
- 5.4.3事件的傳遞(響應鏈)
- 5.4.4 響應者對于事件的操作方式
- 5.4.5 響應鏈中的事件規則
- 6 UIResponder、UIGestureRecognizer、UIControl
- 6.1 UIGestureRecognizer(手勢識別器)
- 6.2 持續行手勢(UIPanGestureRecognizer)
- 6.3 手勢識別器中的三個屬性
- 6.3.1 cancelsTouchesInView
- 6.3.2 delaysTouchesBegan
- 6.3.3 delaysTouchesEnded
- 6.4 UIControl
- 6.4.1 target-action
- 6.5 觸摸事件的優先級
- 7. 總結
本文學習的內容大致包括:
- 觸摸事件由觸屏生成后是如何傳遞到當前應用的
- 應用接收觸摸事件后如何尋找最佳響應者?實現原理?
- 觸摸事件如何沿著響應鏈流動?
- 響應鏈、手勢識別器、UIControl之間對于觸摸事件的響應有著什么樣的關系?
1. 事件的聲明周期
當指尖觸摸屏幕的那一刻,一個觸摸事件就在系統中生成了。經過IPC進程通信,事件最終被傳遞到合適的應用。在經歷過一些過程之后,最終被釋放,流程如下:
2. 系統相應階段
mach port 進程端口,各進程之間通過它進行通信。
SpringBoad.app 是一個系統進程,可以理解為桌面系統,可以統一管理和分發系統接收到的觸摸事件。(學過逆向的應該知道)
此時SpringBoard會根據當前桌面的狀態,判斷應該由誰處理此次觸摸事件。因為事件發生時,你可能正在桌面上翻頁,也可能正在刷微博。若是前者(即前臺無APP運行),則觸發SpringBoard本身主線程runloop的source0事件源的回調,將事件交由桌面系統去消耗;若是后者(即有app正在前臺運行),則將觸摸事件通過IPC傳遞給前臺APP進程,接下來的事情便是APP內部對于觸摸事件的響應了。
3. APP響應階段
觸摸事件從觸屏產生后,由IOKit將觸摸事件傳遞給SpringBoard進程,再由SpringBoard分發給當前前臺APP處理。
4. 觸摸、事件、響應者
4.1 UITouch(觸摸)
- 一個手指一次觸摸屏幕,就對應生成一個UITouch對象。多個手指同時觸摸,生成多個UITouch對象。
- 多個手指先后觸摸,系統會根據觸摸的位置判斷是否更新同一個UITouch對象。若兩個手指一前一后觸摸同一個位置(即雙擊),那么第一次觸摸時生成一個UITouch對象,第二次觸摸更新這個UITouch對象(UITouch對象的 tap count 屬性值從1變成2);若兩個手指一前一后觸摸的位置不同,將會生成兩個UITouch對象,兩者之間沒有聯系
- 每個UITouch對象記錄了觸摸的一些信息,包括觸摸時間、位置、階段、所處的視圖、窗口等信息
- 當手指離開屏幕后,確定一段時間UITouch不會再更新之后將被釋放
4.2 UIEvent(事件真身)
- 觸摸的目的是生成觸摸事件供響應者響應,一個觸摸事件對應一個UIEvent對象,其中的type標識了事件的類型
- UIEvent對象中包含了觸發該事件的觸摸對象集合,因為一個觸摸事件可能由多個手指同時觸摸產生的,觸摸對象集合通過allTouches屬性獲取
4.3 UIResponder(響應者)
每一個響應者都是一個UIResponder對象,及所有派生自UIResponder的對象,本身都具備響應事件的能力,因此一下類的實例都是響應者都是實例:
- UIView
- UIViewController
- UIApplication
- AppDelegate
響應者之所以能夠響應事件,是因為提供了4個觸摸事件的方法
//手指觸碰屏幕,觸摸開始 - (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;這個幾個方法在響應者對象接收到事件的調用,用于做出對事件的響應。關于響應者何時接收到事件以及事件如何沿著響應鏈傳遞
5. 尋找事件的最佳響應者(Hit-Testing)
每個事件的理想宿命是被能夠響應它的對象響應后釋放,然而響應者諸多,事件一次只有一個,誰都想把事件搶到自己碗里來,為避免紛爭,就得有一個先后順序,也就是得有一個響應者的優先級。因此這就存在一個尋找事件最佳響應者(又稱第一響應者 first responder)的過程,目的是找到一個具備最高優先級響應權的響應對象(the most appropriate responder object),這個過程叫做Hit-Testing,那個命中的最佳響應者稱為hit-tested view。
當APP通過mach port得到這個觸摸事件時,APP中有那么多UIView或者UIViewController,到底應該給誰去響應呢?尋找最佳響應者就是找出這個優先級最高的響應對象。
5.1 事件自下而上的傳遞
應用接收到事件后先將其置入事件隊列中以等待處理。出隊后,application首先將事件傳遞給當前應用最后顯示的窗口(UIWindow)詢問其能否響應事件,若窗口能響應事件,則傳遞給子視圖詢問是否能響應,子視圖若能響應則繼續詢問子視圖。子視圖詢問的順序是優先詢問后添加的子視圖,即子視圖數組中靠后的視圖。事件傳遞順序如下
UIApplication ——> UIWindow ——> 子視圖 ——> ... ——> 子視圖事實上吧UIwindow也看成是視圖即可,這個過程就是一個遞歸詢問子視圖能否響應事件的過程,且后添加的子視圖優先級高(對于window而言就是后顯示的window優先級高)
事件的具體流程:
視圖層級如下(同一層級的視圖越在下面,表示越后添加):
A ├── B │ └── D └── C├── E└── F現在假設在E視圖所處的屏幕位置觸發一個觸摸,應用接收到這個觸摸事件事件后,先將事件傳遞給UIWindow,然后自下而上開始在子視圖中尋找最佳響應者。事件傳遞的順序如下所示:
5.2 Hit-Testing的本質
上面講了事件在響應者之間傳遞的規則,視圖通過判斷自身能否響應事件來決定是否繼續向子視圖傳遞。那么問題來了:視圖如何判斷能否響應事件?以及視圖如何將事件傳遞給子視圖?
那么我們首先需要知道,以下幾種狀態是無法響應事件:
- 不允許交互:userInteractionEnabled = NO
- 隱藏:hidden = YES 如果父視圖隱藏,那么子視圖也會隱藏,隱藏的視圖無法接收事件
- 透明度:alpha < 0.01 如果設置一個視圖的透明度<0.01,會直接影響子視圖的透明度。alpha:0.0~0.01為透明。
hitTest:withEvent:
每個UIView對象都有一個 hitTest:withEvent: 方法,這個方法是Hit-Testing過程中最核心的存在,其作用是詢問事件在當前視圖中的響應者,同時又是作為事件傳遞的橋梁。
hitTest:withEvent: 返回一個UIView對象,作為當前視圖層次中的響應者。
其方法默認實現:
- 若當前視圖無法響應事件,則返回nil
- 若當前視圖可以響應事件,但無子視圖可以響應事件,則返回自身作為當前視圖層次中的事件響應者
- 若當前視圖可以響應事件,同時有子視圖可以響應,則返回子視圖層次中的事件響應者
一開始UIApplication將事件通過調用UIWindow對象的hitTest:withEvent:傳遞給UIWindown對象,UIWindow的 hitTest:withEvent:在執行時若判斷本身能響應事件,則調用子視圖的 hitTest:withEvent:將事件傳遞給子視圖并詢問子視圖上的最佳響應者。最終UIWindow返回一個視圖層次中的響應者視圖給UIApplication,這個視圖就是hit-testing的最佳響應者。
系統對于視圖能否響應事件的判斷邏輯除了之前提到的3種限制狀態,默認能響應的條件就是觸摸點在當前視圖的坐標系范圍內。因此,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。
現在我們在上述示例的視圖層次中的每個視圖類中添加下面3個方法來驗證一下之前的分析(注意 :hitTest:withEvent: 和 pointInside:withEvent: 方法都要調用父類的實現,否則不會按照默認的邏輯來執行Hit-Testing)
- (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__); }那么單擊E視圖,調用過程如下:
可以看到最終是視圖E對事件進項了響應, 同時事件的傳遞過程也和上面分析的一致,事實上單擊從[AView hitTest:withEvent:]到[EView pointInside:withEvent:]的過程會執行兩邊,兩次傳的是同一個Touch,區別在Touch的狀態不同,第一次是begin狀態,第二階段是end階段。也就是說對于事件的傳遞起源與觸摸狀態的變化
5.3 Hit-Testing過程中的事件攔截(自定義事件流向)
實際開發過程中可能會遇到一些特殊的交互需求,需要定制視圖對于事件的響應,例如下面
G和H視圖都是跟控制器視圖的子視圖,I視圖是添加在H視圖上,當我們觸摸I視圖在G視圖中的那部分時,我們看打印結果:
通過打印結果我們可以發現,事件根本沒有傳遞到I視圖這里,這是為什么了?
原來觸摸事件最早傳遞到H視圖,然后調用H視圖的[HView hitTest:withEvent:],在這個方法中會調用[HView pointInside:withEvent:],判斷觸摸點是否在視圖范圍內,這里由于觸摸點在G視圖的那部分,所以不在H視圖,因此該方法返回NO,這樣H視圖的事件傳遞就結束了,于是事件就傳遞到了G視圖內,由于G視圖可以響應觸摸事件,而且G視圖內沒有子視圖,所以G視圖就是事件的最佳響應者。
那么這顯示不是我們想要的結果,我們希望當觸摸I視圖時,不管觸摸I視圖的哪里,I視圖都能成為最佳響應者響應事件
要解決這個問題也很簡單, 我們可以重寫H視圖中的pointInside:withEvent:方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{NSLog(@"%s", __func__);CGPoint tmpPoint = [self convertPoint:point toView:_iView];if([_iView pointInside:tmpPoint withEvent:event]){return YES;}return [super pointInside:point withEvent:event]; }我們判斷觸摸點位置是否在視圖I范圍內,如果在視圖I的范圍內,則直接返回YES,這樣一來I視圖就可以響應觸摸事件了
5.4 事件的響應以及在響應鏈中的傳遞
經過Hit-Testing后,UIApplication已經知道了事件的最佳響應者是誰了,那么接下來要做的事情就是:
- 將事件傳遞給最佳響應者
- 事件沿著響應鏈傳遞
5.4.1 事件響應的前奏
因為最佳響應者具有最高的事件響應優先級,因此UIApplication會先將事件傳遞給其它響應者。首先UIApplication將時間通過sendEvent:傳遞給事件所屬的Window,window同樣通過sendEvent:再將事件傳遞給hit-tested view,即最佳響應者
UIApplication ——> UIWindow ——> hit-tested view以事件自下而上的傳遞結點中為例,我們在E視圖中的touchesBegan:withEvent:斷點,然后查看調用棧就能看清除這一過程
那么問題來了,這個過程中,假如應用中存在多個window對象,UIApplication是怎么知道要把事件傳給哪個window的?window又是怎么知道哪個視圖才是最佳響應者的呢?
其實簡單思考一下,這兩個過程都是傳遞事件的過程,涉及的方法都是 sendEvent: ,而該方法的參數(UIEvent對象)是唯一貫穿整個經過的線索,那么就可以大膽猜測必然是該觸摸事件對象上綁定了這些信息。事實上之前在介紹UITouch的時候就說過touch對象保存了觸摸所屬的window及view,而event對象又綁定了touch對象,如此一來,是不是就說得通了。要是不信的話,那就自定義一個Window類,重寫 sendEvent: 方法,捕捉該方法調用時參數event的狀態,答案就顯而易見了。
至于這兩個屬性是什么時候綁定到touch對象上的,必然是在hit-testing的過程中唄,仔細想想hit-testing干的不就是這個事兒嗎~
5.4.2 事件的響應
前面介紹UIResponsder的時候說過,每個響應者必定都是UIResponder對象,通過4個響應觸摸事件的方法來響應事件。每個UIResponder對象默認都已經實現了這4個方法,但是默認不對事件做任何處理,單純只是將事件沿著響應鏈傳遞。若要截獲事件進行自定義的響應操作,就要重寫相關的方法。例如,通過重寫 touchesMoved: withEvent: 方法實現簡單的視圖拖動。
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;每個響應觸摸事件的方法會接收兩個參數,分別對應觸摸對象集合和事件對象。通過監聽觸摸對象中保存觸摸點位置的變動,可以時時修改視圖的位置。視圖(UIView)作為響應者對象,本身已經實現了 touchesMoved: withEvent: 方法,因此要創建一個自定義視圖(繼承自UIView),重寫該方法。
//MovedView //重寫touchesMoved方法(觸摸滑動過程中持續調用) - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {//獲取觸摸對象UITouch *touch = [touches anyObject];//獲取前一個觸摸點位置CGPoint prePoint = [touch previousLocationInView:self];//獲取當前觸摸點位置CGPoint curPoint = [touch locationInView:self];//計算偏移量CGFloat offsetX = curPoint.x - prePoint.x;CGFloat offsetY = curPoint.y - prePoint.y;//相對之前的位置偏移視圖self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY); }每個響應者都有權決定是否執行事件的響應,只要重寫觸摸事件方法即可
5.4.3事件的傳遞(響應鏈)
前面一直說的最佳響應者,之所以被稱為最佳,是其具備響應事件的最高優先權。最佳響應者首先接收到事件,然后便擁有對事件的絕對控制權,即它可以選擇獨吞這個事件,也可以把這個事件像下傳遞給其它響應者,這個由響應者構成的視圖鏈就稱之為響應鏈。
需要注意的是,上一節中也說到了事件的傳遞,與此處所說的事件的傳遞有本質區別。上一節所說的事件傳遞的目的是為了尋找事件的最佳響應者,是自下而上的傳遞;而這里的事件傳遞目的是響應者做出對事件的響應,這個過程是自上而下的。前者為“尋找”,后者為“響應”。
5.4.4 響應者對于事件的操作方式
響應者對于事件的攔截以及傳遞都是通過touchesBegan:withEvent:方法控制的,該方法默認實現是將事件沿著默認的響應鏈往下傳遞。
響應者對于接收事件有一下3種操作:
- 不攔截,默認操作:事件會沿著默認的響應鏈往下傳遞
- 攔截,不在往下分發事件:重寫touchesBegan:withEvent:進行事件處理,不調用父類的touchesBegan:withEvent:方法
- 攔截,繼續往下分發事件:重寫touchesBegan:withEvent:進行事件處理,同時調用父類的touchesBegan:withEvent:方法往下傳遞事件
5.4.5 響應鏈中的事件規則
每一個響應者(UIResponder對象)都有一個 nextResponder 方法,用于獲取響應鏈中當前對象的下一個響應者,因此一旦事件的最佳響應者確定了,這個事件的響應鏈就確定了
對于響應者,默認的 nextResponder 實現如下:
- UIView:如果視圖是控制器的根視圖,則其nextResponder為控制器對象;否則,其nextResponder為父視圖。
- UIViewController:若控制器的視圖是window的根視圖,則其nextResponder為窗口對象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller
- UIWindow:nextResponder為UIApplication對象。
- UIApplication:若當前應用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。
上圖是官網對于響應鏈的展示,若觸摸發生在UITextField上,則事件的傳遞順序是:
UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對象
可以使用一下方式打印一個響應鏈上每一個響應者對象, 在最佳響應者的touchBegin:withEvent:方法中調用即可(別忘了調用父類的方法)
我們以上面事件攔截中的案例來看看:
上面結果打印出了完整的響應鏈,另外如果有需要,完全可以重寫響應者的nextResponder方法來自定義響應鏈
6 UIResponder、UIGestureRecognizer、UIControl
IOS中,除了UIResponder能夠響應事件,手勢識別器、UIControl同樣具備對事件的處理能力。
6.1 UIGestureRecognizer(手勢識別器)
我們首先來看一個場景,給下圖中的黃色View(AxsView)添加一個點擊手勢:
查看運行結果:
從日志上看出YellowView最后Cancel了對觸摸事件的響應,而正常應當是觸摸結束后,AxsView的touchesEnded:withEvent:的方法被調用才對。另外,期間還執行了手勢識別器綁定的action 。我從官方文檔找到了這樣的解釋:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
大致理解是,Window在將事件傳遞給hit-tested view之前,會先將事件傳遞給相關的手勢識別器并由手勢識別器優先識別。若手勢識別器成功識別了事件,就會取消hit-tested view對事件的響應;若手勢識別器沒能識別事件,hit-tested view才完全接手事件的響應權。
一句話概括:手勢識別器比UIResponder具有更高的事件響應優先級!!!!
按照這個解釋,Window在將事件傳遞給hit-tested view即AxsView之前,先傳遞給了控制器根視圖上的手勢識別器。手勢識別器成功識別了該事件,通知Application取消AxsView對事件的響應。
然而看日志,卻是AxsView的 touchesBegan:withEvent: 先調用了,既然手勢識別器先響應,不應該上面的action先執行嗎,這又怎么解釋?事實上這個認知是錯誤的。手勢識別器的action的調用時機(即此處的 actionTapView)并不是手勢識別器接收到事件的時機,而是手勢識別器成功識別事件后的時機,即手勢識別器的狀態變為UIGestureRecognizerStateRecognized。因此從該日志中并不能看出事件是優先傳遞給手勢識別器的,那該怎么證明Window先將事件傳遞給了手勢識別器?
要解決這個問題,只要知道手勢識別器是如何接收事件的,然后在接收事件的方法中打印日志對比調用時間先后即可。說起來你可能不信,手勢識別器對于事件的響應也是通過這4個熟悉的方法來實現的。
- (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;需要注意的是,雖然手勢識別器通過這幾個方法來響應事件,但它并不是UIResponder的子類,相關的方法聲明在 UIGestureRecognizerSubclass.h 中。
這樣一來,我們便可以自定義一個單擊手勢識別器的類,重寫這幾個方法來監聽手勢識別器接收事件的時機。創建一個UITapGestureRecognizer的子類,重寫響應事件的方法,每個方法中調用父類的實現,并替換demo中的手勢識別器。另外需要在.m文件中引入 import <UIKit/UIGestureRecognizerSubclass.h> ,因為相關方法聲明在該頭文件中。
#import "AxsTapGesture.h" #import <UIKit/UIGestureRecognizerSubclass.h>@implementation AxsTapGesture- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%s",__func__);[super touchesBegan:touches withEvent:event]; }- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%s",__func__);[super touchesEnded:touches withEvent:event]; }- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%s",__func__);[super touchesMoved:touches withEvent:event]; }- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%s",__func__);[super touchesCancelled:touches withEvent:event]; } @end在次點擊AxsView,查看打印結果:
很明顯,確實是手勢識別器先接收到了事件。之后手勢識別器成功識別了手勢,執行了action,再由Application取消了AxsView對事件的響應。
Window怎么知道要把事件傳遞給哪些手勢識別器?
之前探討過Application怎么知道要把event傳遞給哪個Window,以及Window怎么知道要把event傳遞給哪個hit-tested view的問題,答案是這些信息都保存在event所綁定的touch對象上。手勢識別器也是一樣的,event綁定的touch對象上維護了一個手勢識別器數組,里面的手勢識別器毫無疑問是在hit-testing的過程中收集的。打個斷點看一下touch上綁定的手勢識別器數組:
Window先將事件傳遞給這些手勢識別器,再傳給hit-tested view。一旦有手勢識別器成功識別了手勢,Application就會取消hit-tested view對事件的響應。
6.2 持續行手勢(UIPanGestureRecognizer)
將上面demo中的單擊手勢改成滑動手勢,然后我們看下打印結果
先總結一下手勢識別器與UIResponder對于事件響應的聯系:
當觸摸發生或者觸摸的狀態發生變化時,Window都會傳遞事件尋求響應。
- Window先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器,再發送給觸摸對象對應的hit-tested view。
- 手勢識別器識別手勢期間,若觸摸對象的觸摸狀態發生變化,事件都是先發送給手勢識別器再發送給hit-test view。
- 手勢識別器若成功識別了手勢,則通知Application取消hit-tested view對于事件的響應,并停止向hit-tested view發送事件
- 若手勢識別器未能識別手勢,而此時觸摸并未結束,則停止向手勢識別器發送事件,僅向hit-test view發送事件
- 若手勢識別器未能識別手勢,且此時觸摸已經結束,則向hit-tested view發送end狀態的touch事件以停止對事件的響應。
6.3 手勢識別器中的三個屬性
@property(nonatomic) BOOL cancelsTouchesInView; @property(nonatomic) BOOL delaysTouchesBegan; @property(nonatomic) BOOL delaysTouchesEnded;6.3.1 cancelsTouchesInView
默認為YES。表示當手勢識別器成功識別了手勢之后,會通知Application取消響應鏈對事件的響應,并不再傳遞事件給hit-test view。若設置成NO,表示手勢識別成功后不取消響應鏈對事件的響應,事件依舊會傳遞給hit-test view。
上述demo中設置cancelsTouchesInView = NO,滑動打印結果如下:
即便滑動手勢識別器識別了手勢,Application也會依舊發送事件給AxsView。
6.3.2 delaysTouchesBegan
默認為NO。默認情況下手勢識別器在識別手勢期間,當觸摸狀態發生改變時,Application都會將事件傳遞給手勢識別器和hit-tested view;若設置成YES,則表示手勢識別器在識別手勢期間,截斷事件,即不會將事件發送給hit-tested view
設置delaysTouchesBegan = YES
打印結果如下:
因為滑動手勢識別器在識別期間,事件不會傳遞給AxsView,因此期間AxsView的 touchesBegan:withEvent: 和 touchesMoved:withEvent: 都不會被調用;而后滑動手勢識別器成功識別了手勢,也就獨吞了事件,不會再傳遞給AxsView。因此只打印了手勢識別器成功識別手勢后的action調用。
6.3.3 delaysTouchesEnded
默認為YES。當手勢識別失敗時,若此時觸摸已經結束,會延遲一小段時間(0.15s)再調用響應者的 touchesEnded:withEvent:;若設置成NO,則在手勢識別失敗時會立即通知Application發送狀態為end的touch事件給hit-tested view以調用 touchesEnded:withEvent: 結束事件響應。
總結: 手勢識別器比響應鏈具有更高的事件響應優先級。
6.4 UIControl
UIControl是系統提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子類。當UIControl跟蹤到觸摸事件時,會向其上添加的target發送事件以執行action。值得注意的是,UIConotrol是UIView的子類,因此本身也具備UIResponder應有的身份。
關于UIControl此處介紹兩點:
- target-action執行時機及過程
- 觸摸事件優先級
6.4.1 target-action
- target: 處理交互事件的對象
- action:處理交互事件的方式
UIControl作為能夠響應事件的控件,必然也需要待事件交互符合條件時才去響應,因此也會跟蹤事件發生的過程。不同于UIResponder以及UIGestureRecognizer通過 touches 系列方法跟蹤,UIControl有其獨特的跟蹤方式:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event; - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event; - (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; - (void)cancelTrackingWithEvent:(nullable UIEvent *)event;乍一看,這4個方法和UIResponder的那4個方法幾乎吻合,只不過UIControl只能接收單點觸控,因此接收的參數是單個UITouch對象。這幾個方法的職能也和UIResponder一致,用來跟蹤觸摸的開始、滑動、結束、取消。不過,UIControl本身也是UIResponder,因此同樣有 touches 系列的4個方法。事實上,UIControl的 Tracking 系列方法是在 touch 系列方法內部調用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法內部調用的, 因此它雖然也是UIResponder,但 touches 系列方法的默認實現和UIResponder本類還是有區別的。
當UIControl跟蹤事件的過程中,識別出事件交互符合響應條件,就會觸發target-action進行響應。UIControl控件通過 addTarget:action:forControlEvents: 添加事件處理的target和action,當事件發生時,UIControl通知target執行對應的action。說是“通知”其實很籠統,事實上這里有個action傳遞的過程。當UIControl監聽到需要處理的交互事件時,會調用 sendAction:to:forEvent: 將target、action以及event對象發送給全局應用,Application對象再通過 sendAction:to:from:forEvent: 向target發送action。
因此,可以通過重寫UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定義事件執行的target及action。
另外,若不指定target,即 addTarget:action:forControlEvents: 時target傳空,那么當事件發生時,Application會在響應鏈上從上往下尋找能響應action的對象。官方說明如下
If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.
6.5 觸摸事件的優先級
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:
A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.
簡單理解:UIControl會阻止父視圖上的手勢識別器行為,也就是UIControl處理事件的優先級比UIGestureRecognizer高,但前提是相比于父視圖上的手勢識別器
預設場景:在AxsView上添加一個button(AxsButton),同時給button添加一個target-action事件
- 示例一:在AxsView上添加點擊手勢識別器
- 示例二:在button上添加手勢識別器
操作方式: 單擊button
測試結果:示例一中button的target-action響應了單擊事件;示例二中:AxsView上的手勢識別器響應了事件。過程日志打印如下:
- 原因分析: 點擊button后,事件先傳遞給手勢識別器,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder,這一過程和普通事件響應者無異)。示例一中,由于button阻止了父視圖BlueView中的手勢識別器的識別,導致手勢識別器識別失敗,button完全接手了事件的響應權,事件最終由button響應;示例二中:button未阻止其本身綁定的手勢識別器的識別,因此手勢識別器先識別手勢并識別成功,而后通知Application取消響應鏈對事件的響應,因為 touchesCancelled 被調用,同時 cancelTrackingWithEvent 跟著調用,因此button的target-action得不到執行。
- 其他:經測試,若示例一中的手勢識別器設置 cancelsTouchesInView 為NO,手勢識別器和button都能響應事件。也就是說這種情況下,button不會阻止父視圖中手勢識別器的識別。
- UIControl會阻止父視圖上的手勢識別器的行為,也就是UIControl比其父視圖上的手勢識別器具有更高的事件響應優先級。(但是僅限于系統提供的有默認action操作的UIControl,例如UIbutton、UISwitch等的單擊,而對于自定義的UIControl,經驗證,響應優先級比手勢識別器低。)但是比UIControl自身的UIGestureRecognizer優先級要低。
7. 總結
- 觸摸發生時,系統內核生成觸摸事件,先由IOKit處理封裝成IOHIDEvent對象,通過IPC傳遞給系統進程SpringBoard,而后再傳遞給前臺APP處理
- 事件傳遞到APP內部時被封裝成開發者可見的UIEvent對象,先經過hit-testing尋找第一響應者,而后由Window對象將事件傳遞給hit-tested view,并開始在響應鏈上的傳遞。
- UIRespnder、UIGestureRecognizer、UIControl,籠統地講,事件響應優先級依次遞增。
總結
以上是生活随笔為你收集整理的IOS中触摸事件学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 硬盘智能搜索匹配技术研究与实现
- 下一篇: Node代码转换为Js