iOS學習
- 響應者鏈和事件傳遞鏈
- 傳遞鏈:
- hitTest:withEvent
- **pointInside:withEvent**
- 響應鏈
- 第一響應者和最佳響應者
- 觸摸事件(UITouch)
- UIGestureRecognizer(手勢識別器)
響應者鏈和事件傳遞鏈
iOS事件的主要由:響應連和提交鏈構成。一般事件先通過提交鏈,提交下去。響應鏈,如果上層不能響應,那么一層通過響應鏈找到能響應的UIResponse
。
-
響應連:由最基礎的
view
向系統提交,first view
->super view
-> … ->view controller
->window
->Application
->AppDelegate
-
交付鏈:有系統向最上層
view
交付,Application
->window
->root view
-> … ->first view
iOS中只有繼承了UIResponse
的對象才能夠接受處理事件。UIResponse
是響應對象的基類,定義了處理上述各種事件的接口。常見的子類有:UIView
,UIViewController
,UIApplication
和UIApplicationDelegate
。
穿透控件:
如果我們不想讓某個視圖響應事件,只需要重載 PointInside:withEvent:方法,讓此方法返回NO就行了.
若是view上有view1,view1上有view2,點擊view2,view2自己響應,點擊view1,view1不響應,只有view響應,也就是隔層傳遞
與加速器,陀螺儀和磁力計相關的運動事件不遵循響應者連,Core Motion
將這些事件直接傳遞給你指定的對象
傳遞鏈:
事件傳遞流程:
- 發生觸摸事件后,系統會將該事件封裝成
UIEvent
對象加入到一個由UIApplication管理的事件隊列
UIApplication
會從事件隊列中取出最前面的事件,并將事件分發下去以便處理,通常,先發送事件給應用程序的主窗口(keyWindow)
。- 主窗口會調用
hitTest:withEvent:
方法沿著視圖層次結構從上到下進行傳遞最后在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個事件處理過程的第一步。 - 找到合適的視圖控件后,就會調用視圖控件的touches方法(touchesBegan、touchesMoved、touchedEnded)來作具體的事件處理。
觸摸事件的傳遞是從父控件傳遞到子控件
也就是UIApplication->window->尋找處理事件最合適的view
觸摸事件的傳遞是從父控件傳遞到子控件,如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件
上文中說到:要找到最合適的控件來處理事件,那么,如何找到呢?
找到最合適的控件:
- 自己是否能接受觸摸事件?
- 觸摸點是否在自己身上
- 通過
pointInside:withEvent
方法判斷觸摸點是否在自己身上。返回NO則不在自己身上,那就不再遍歷子控件,返回YES,代表在自己身上,那就繼續遍歷子控件,從后往前遍歷子控件,重復前面兩個步驟如果沒有符合條件的子控件,那么自己就是最適合處理的控件,找到最適合接受的控件后,調用控件touchesBegan,touchesMoved,touchedEnded的方法。
- 通過
- 從后往前遍歷子控件,重復前面的兩個步驟歐
- 如果沒有符合條件的子控件,那么就自己最適合處理。
UIView不接收觸摸事件的三種情況:
- userInteractionEnabled = NO隱藏
- hidden = YES;
- 透明:alpha = 0.0 ~ 0.01;
具體的尋找,使用了兩個方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent
只要事件一傳遞給一個控件,這個控件就會調用他自己的hitTest:withEvent:
方法
為了尋找并返回最合適的view(能夠響應時間的那個最合適的view)
以下是這個方法的實現邏輯
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {// 1.判斷窗口能否接收事件if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;// 2.判斷點在不在窗口上// 不在窗口上if ([self pointInside:point withEvent:event] == NO) return nil;// 3.從后往前遍歷子控件數組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) {// 如果能找到最合適的viewreturn fitView;}}// 4.沒有找到更合適的view,也就是沒有比自己更合適的viewreturn self;}
- 首先判斷該控件是否能接受事件
- 在調用當前視圖的
pointInside:withEvent:
方法判斷觸摸點是否在當前視圖內 - 返回NO,則表示不在該視圖內,直接返回nil
- 若返回YES,則向當前視圖的所有子視圖發送
hitTest:withEvent:
消息,所有子視圖的遍歷順序是從最頂視圖一直到最低層視圖,即從subviews
數組的末尾向前遍歷,直到有子視圖返回非空對象,或者全部子視圖遍歷完畢。 - 若第一次有子視圖返回非空對象,則
hitTest:withEvent:
返回此對象,處理結束。 - 若所有子視圖都返回空,則
hitTest:withEvent:
返回自身
不管這個控件能不能處理事件,也不管觸摸點在不在這個控件上,事件都會先傳遞給這個控件,隨后再調用
hitTest:withEvent:
方法如果
hitTest:withEvent:
方法中返回nil,那么調用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒有找到更合適的view。那么最合適的view就是該控件的父控件。
其實該方法的邏輯很簡單,就是先判斷自己能不能響應,是不是在自己身上,不是則直接返回。是的話繼續往下遞歸判斷子事件,直到返回nil,nil之前的父控件則是最合適的控件。
pointInside:withEvent
該方法就是判斷點是不是在當前view上的。返回YES,代表點在方法調用者的坐標系上;返回NO,代表點不在方法調用者的坐標系上,那么方法調用者也就不能處理事件。
解決以下問題,點擊紅色區域時,按鈕并不會接受這個事件
//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{//將觸摸點坐標轉換到在CircleButton上的坐標CGPoint pointTemp = [self convertPoint:point toView:_CircleButton];//若觸摸點在CricleButton上則返回YESif ([_CircleButton pointInside:pointTemp withEvent:event]) {return YES;}//否則返回默認的操作return [super pointInside:point withEvent:event];
}
響應鏈
事件響應流程:
- 如果找到最合適的控件來處理調用最合適的控件的touches…(touchesBegan、touchesMoved、touchedEnded)方法。
- 如果調用了[super touch…],就會將事件順著響應者鏈往上傳遞,傳給上一個響應者,接著上一個響應者就會調用touches…方法。
- 如果沒有找到合適的控件來處理事件,則將事件傳回來窗口,窗口不處理事件,將事件傳給 UIApplication。如果 UIApplication 不能處理事件,則將其丟棄。
響應者
在iOS中,不是任何對象都能處理事件,只有繼承了UIResponder的對象才能接收并處理事件,稱之為“響應者對象”。
UIApplication、UIViewController、UIView都繼承自UIResponder,因此它們都是響應者對象,都能夠接收并處理事件。
UIResponder提供了我們平時最常用的touchesBegan/touchesMoved/touchesEnded方法。此外還有如下幾個屬性比較重要:
- isFirstResponder:判斷該View是否為第一響應者。
- canBecomeFirstResponder:判斷該View是否可以成為第一響應者。
- becomeFirstResponder:使該View成為第一響應者。
- resignFirstResponder:取消View的第一響應者。
這里我們需要區分一下第一響應者和最佳響應者
第一響應者和最佳響應者
-
第一響應者 (First Responder):
- 第一響應者是指當前能夠響應某個事件的第一個對象。
- 通常情況下,當某個事件發生時,該事件首先被傳遞到第一響應者。
- 第一響應者通常是用戶當前正在交互的視圖,比如用戶正在編輯的
UITextField
或者點擊的UIButton
。
-
最佳響應者(Best Responder):
- 最佳響應者是指在響應者鏈上最適合處理某個事件的對象。
- 當第一響應者無法完全處理某個事件時,該事件會沿著響應者鏈向上傳遞,直到找到最佳響應者。
- 最佳響應者通常是能夠最完整地處理該事件的對象,比如包含第一響應者的視圖控制器。
一般來說,事件傳遞的目的就是為了讓我們找到第一響應者。那么我們該如何判斷是否為第一響應者呢?
- 能夠響應觸摸事件
- 觸摸點在自己身上
- 沒有任何子視圖,或是所有子視圖都不在觸摸點上
補充知識:
對于UIWindow,若存在多個窗口,則優先詢問后顯示的窗口,這和控件的邏輯是一致的,優先詢問子視圖和后出現的控件
什么是上一個響應者?
如果當前這個view是控制器的view,那么控制器就是上一個響應者;
如果當前這個view不是控制器的view,那么父控件就是上一個響應者。
我們知道,響應者是從下往上傳的
響應者對于事件的操作方式:
響應者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent:
方法控制的,該方法的默認實現是將事件沿著默認的響應鏈往下傳遞。
響應者鏈條是什么
它是一種事件處理機制,由多個響應者對象連接起來的層次結構,使得事件可以沿著這些對象進行傳遞。利用響應者鏈條我們可以通過調用touches的super 方法,讓多個響應者同時響應該事件。
如何做到多個對象處理同一個事件
因為系統默認做法是把事件上拋給父控件,所以可以通過重寫自己的touches方法和父控件的touches方法來達到一個事件多個對象處理的目的
iOS中的各種事件
- 觸摸事件
- 加速計事件
- 遠程控制事件
與加速器,陀螺儀和磁力計相關的運動事件不遵循響應者連,Core Motion
將這些事件直接傳遞給你指定的對象
觸摸事件(UITouch)
保存著跟手指相關的信息,比如觸摸的位置、時間、階段。 當手指移動時,系統會更新同一個UITouch對象,使之能夠一直保存該手指在的觸摸位置。 當手指離開屏幕時,系統會銷毀相應的UITouch對象。
UITouch的常用屬性和方法
@property(nonatomic,readonly,retain) UIWindow *window;
//觸摸產生時所處的窗口
@property(nonatomic,readonly,retain) UIView *view;
//觸摸產生時所處的視圖
@property(nonatomic,readonly) NSUInteger tapCount;
//短時間內點按屏幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSTimeInterval timestamp;
//記錄了觸摸事件產生或變化時的時間,單位是秒
@property(nonatomic,readonly) UITouchPhase phase;
//當前觸摸事件所處的狀態- (CGPoint)locationInView:(UIView *)view;
//返回值表示觸摸在view上的位置,這里返回的位置是針對view的坐標系的(以view的左上角為原點(0, 0));調用時傳入的view參數為nil的話,返回的是觸摸點在UIWindow的位置。
- (CGPoint)previousLocationInView:(UIView *)view;
//該方法記錄了前一個觸摸點的位置。
UIEvent
UIEvent:稱為事件對象,記錄事件產生的時刻和類型。 每產生一個事件,就會產生一個UIEvent對象。 UIEvent還提供了相應的方法可以獲得在某個view上面的觸摸對象(UITouch)。event綁定了touch對象
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件類型
@property(nonatomic,readonly) NSTimeInterval timestamp;
//事件產生的時間
觸摸過程:
一次完整的觸摸過程,會經歷一下三個狀態:
- 觸摸開始:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- 觸摸移動:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- 觸摸結束:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
如果取消了這次觸摸,還會有一個觸摸取消方法:
- 觸摸取消(可能會經歷):
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
這四個觸摸事件處理方法中,都有NSSet *touches
和UIEvent *event
兩個參數。
用戶用手指觸摸屏幕時,會創建一個與手指相關聯的UITouch對象。一根手指對應一個UITouch對象。
-
一次完整的觸摸過程中,只會產生一個事件對象,4個觸摸方法都是同一個event參數。
-
如果兩根手指同時觸摸一個view,那么view只會調用一次
touchesBegan:withEvent:
方法,touches參數中裝著2個UITouch對象。 -
如果這兩根手指一前一后分開觸摸同一個view,那么view會分別調用2次
touchesBegan:withEvent:
方法,并且每次調用時的touches參數中只包含一個UITouch對象。 -
根據touches中UITouch的個數可以判斷出是單點觸摸還是多點觸摸。
touches中存放的是UITouch對象,touch對象保存了觸摸所屬的window屬性和view屬性。同時event也綁定了touch對象。
UIGestureRecognizer(手勢識別器)
手勢識別器比UIResponder具有更高的事件響應優先級!!
UIGestureRecognizer能識別用戶在某個view上面做的一些常見手勢。該類是一個抽象類,定義了所有手勢的基本行為,使用它的子類才能處理具體的手勢。
事實上,手勢分為離散型手勢(discrete gestures)
和持續型手勢(continuous gesture)
。系統提供的離散型手勢包括點按手勢(UITapGestureRecognizer)
和輕掃手勢(UISwipeGestureRecognizer)
,其余均為持續型手勢
。
UITapGestureRecognizer
(敲擊) UIPinchGestureRecognizer
(捏合,用于縮放) UIPanGestureRecognizer
(拖拽)UISwipeGestureRecognizer
(輕掃) UIRotationGestureRecognizer
(旋轉) UILongPressGestureRecognizer
(長按)
以下是UIGestureRecognizer定義
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {// 沒有觸摸事件發生,所有手勢識別的默認狀態UIGestureRecognizerStatePossible,// 一個手勢已經開始但尚未改變或者完成時UIGestureRecognizerStateBegan,// 手勢狀態改變UIGestureRecognizerStateChanged,// 手勢完成UIGestureRecognizerStateEnded,// 手勢取消,恢復至Possible狀態UIGestureRecognizerStateCancelled, // 手勢失敗,恢復至Possible狀態UIGestureRecognizerStateFailed,// 識別到手勢識別UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
手勢識別器會搶占UITableView的cell的點擊事件。
UITapGestureRecognizer 默認 cancelsTouchesInView = YES:識別成功后會調用 touchesCancelled,中斷 TableView 內部的點擊流程,didSelectRowAtIndexPath可能不觸發或只高亮不選擇。
總結
- 觸摸發生時,系統內核生成觸摸事件,先由IOKit處理封裝成IOHIDEvent對象,通過IPC傳遞給系統進程SpringBoard,而后再傳遞給前臺APP處理。
- 事件傳遞到APP內部時被封裝成開發者可見的UIEvent對象,先經過hit-testing尋找第一響應者,而后由Window對象將事件傳遞給hit-tested view,并開始在響應鏈上的傳遞。
- UIRespnder、UIGestureRecognizer、UIControl,籠統地講,事件響應優先級依次遞增。