iOS中的事件可以分為三大類:觸摸事件、加速計事件、遠程控制事件。
UIResponder父類是NSObject
// 下一個接收事件的響應者 - (nullable UIResponder*)nextResponder; // 是否能成為第一響應者,默認NO,和becomeFirstResponder配合使用 - (BOOL)canBecomeFirstResponder; // 是否成為第一響應者,和canBecomeFirstResponder配合使用 - (BOOL)becomeFirstResponder; // 是否能取消第一響應者,默認YES,和resignFirstResponder配合使用 - (BOOL)canResignFirstResponder; // 是否取消第一響應者,和canResignFirstResponder配合使用 - (BOOL)resignFirstResponder; // 判斷是否是第一響應者 - (BOOL)isFirstResponder; // 觸摸API,一般用於有觸摸屏的設備 // 通知調用者當有一根或者多根手指觸摸到了視圖或者窗口時調用 // 必須把YES傳入當前視圖實例的setMultipleTouchEnabled消息,才會接收多點觸摸事件 - (void)touchesBegan:(NSSet*)touches withEvent:(nullable UIEvent *)event; // 當有一根或者多根手指在視圖或者窗口上觸發移動事件時調用(隨著手指的移動,會持續調用該方法) - (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event; // 當有一根或者多根手指移開了視圖或者窗口時調用 - (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event; // 當系統發出取消觸摸事件的時候調用(比如低內存消耗的警告框、有電話呼入) - (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event; // 深按API,一般用於遙控器 // 開始按壓的時候調用 - (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); // 按壓改變的時候調用 - (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); // 按壓結束的時候調用 - (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); // 當系統發出取消按壓事件的時候調用 - (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); // 加速計API,一般用於可以產生加速計事件的設備,如微信的搖一搖功能 // 開始加速的時候調用 - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); // 加速結束的時候調用 - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); // 系統發出取消加速計事件的時候調用 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); // 遠程控制API,一般用於耳機 // 接收到遠程控制消息時執行 - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
UITouch父類是NSObject
注意:
1. 當用戶用一根手指觸摸屏幕時,會創建一個與手指相關聯的UITouch對象,一根手指對應一個UITouch對象,保存著跟手指相關的信息,比如觸摸的位置、時間、階段
2. 當手指移動時,系統會更新同一個UITouch對象,使之能夠一直保存該手指在的觸摸位置
3. 當手指離開屏幕時,系統會銷毀相應的UITouch對象
4. iPhone開發中,要避免使用雙擊事件
// 記錄了觸摸事件產生或變化時的時間,單位是秒,NSTimeInterval實質是double @property(nonatomic,readonly) NSTimeInterval timestamp; // 當前觸摸事件所處的狀態 @property(nonatomic,readonly) UITouchPhase phase; // 短時間內點按屏幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點擊 @property(nonatomic,readonly) NSUInteger tapCount; // 觸摸類型 @property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0); // 觸摸產生時所處的窗口 @property(nullable,nonatomic,readonly,strong) UIWindow *window; // 觸摸產生時所處的視圖 @property(nullable,nonatomic,readonly,strong) UIView *view; // 獲取事件響應者的所有手勢所組成的數組 @property(nullable,nonatomic,readonly,copy) NSArray*gestureRecognizers NS_AVAILABLE_IOS(3_2); // 返回值表示觸摸在view上的位置 // 這裡返回的位置是針對view的坐標系的(以view的左上角為原點(0, 0)) // 調用時傳入的view參數為nil的話,返回的是觸摸點在UIWindow的位置 - (CGPoint)locationInView:(nullable UIView *)view; // 該方法記錄了前一個觸摸點的位置 // 表示觸摸在view這個視圖上的位置,這裡返回的位置是針對view的坐標系的 // 調用時傳入的view參數為空的話,返回的是觸摸點在整個窗口的位置 - (CGPoint)previousLocationInView:(nullable UIView *)view;
UIEvent父類是NSObject
注意:
1. 每產生一個觸摸事件,就會產生一個UIEvent對象
2. 產生一個事件,不一定就會產生UIEvent對象,因為按壓事件對應UIPressesEvent對象,加速事件對應UIEvent對象,遠程控制事件對應UIEvent對象
3. UIPressesEvent類是UIEvent的子類
// 事件類型 @property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0); @property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0); // 事件產生的時間 @property(nonatomic,readonly) NSTimeInterval timestamp; // 獲取產生事件的觸摸對象集 - (nullable NSSet*)allTouches; // 獲取在window上產生的觸摸對象集 - (nullable NSSet *)touchesForWindow:(UIWindow *)window; // 獲取在view上產生的觸摸對象集 - (nullable NSSet *)touchesForView:(UIView *)view; // 獲取有手勢gesture的觸摸對象集 - (nullable NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);
當用戶與iPhone的觸摸屏產生互動時,硬件就會探測到物理接觸並且通知操作系統,接著操作系統就會創建相應的事件並且將其傳遞給當前正在運行的應用程序的事件隊列,然後這項事件會被事件循環傳遞給優先響應者,優先響應者是事件被觸發時和用戶交互的控件,比如按鈕、視圖等,如果我們編寫了代碼讓優先響應者處理這種類型的事件,那麼它就會處理這種類型的事件,如:如果是觸摸事件,重寫四個觸摸事件處理方法,系統會自動執行這些方法。
處理完某項事件後,響應者有兩個選項:一是將其丟棄;二是將其傳遞給響應鏈條中的下一個響應者。下一個響應者的地址存儲在當前響應者所包含的變量nextResponder當中。如果優先響應者無法處理一項事件,那麼這項事件就傳遞給下一個響應者,直到這項事件到達能處理它的響應者或者到達響應鏈條的末端,也就是UIApplication對象。UIApplication對象收到一項事件後,也是要麼處理,要麼丟棄。
具體分析:
1.發生觸摸事件後,系統會將該事件加入到一個由UIApplication管理的事件隊列中,UIApplication會從事件隊列中取出最前面的事件,並將事件分發下去以便處理,通常先發送事件給應用程序的主窗口(keyWindow),主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個事件處理過程的第一步,找到合適的視圖控件後,就會調用視圖控件的touches方法來作具體的事件處理。
提示: 如果父控件不能接收觸摸事件,那麼子控件就不可能接收到觸摸事件。
2.如何找到最合適的控件來處理事件?
(1). 判斷自己是否能接收觸摸事件(不能接收事件情況:不接收用戶交互、隱藏、透明度為0.0 ~ 0.01)
提示: UIImageView的userInteractionEnabled默認就是NO,因此UIImageView以及它的子控件默認是不能接收觸摸事件的
(2). 判斷觸摸點是否在自己身上
(3). 從後往前遍歷子控件,重復前面的兩個步驟
(4). 如果沒有找到符合條件的子控件,那麼就認為自己是最適合的控件
// hitTest: withEvent:方法是返回最合適的view // 自己實現hitTest: withEvent:方法 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // 1. 判斷能不能處理該方法 if (self.userInteractionEnabled == NO || self.alpha <=0.01 || self.hidden == YES) return nil; // 2. 判斷點在不在自己上面 if ([self pointInside:point withEvent:event] == NO) return nil; // 3. 遍歷子控件,判斷是否有比自己更合適處理該事件的子控件 NSInteger count = self.subviews.count; for (NSInteger i = count - 1; i >= 0; i--) { // 獲取子控件 UIView *childView = self.subviews[i]; // 轉換點 CGPoint childPoint = [self convertPoint:point toView:childView]; // 遞歸判斷 UIView *view = [childView hitTest:childPoint withEvent:event]; // 如果子控件更合適處理事件,就返回子控件 if (view) return view; } // 4. 沒有比自己更合適處理該事件的子控件,就返回自己 return self; }
1.一次完整的觸摸過程,會經歷3個狀態:
觸摸開始:- (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:(nullable NSSet < UITouch * > * )touches withEvent:(nullable UIEvent *)event;
2.四個觸摸事件處理方法中,都有(NSSet < UITouch * > * )touches和(nullable UIEvent * )event兩個參數,在一次完整的觸摸過程中,只會產生一個事件對象,四個觸摸方法都是同一個event參數
3.如果兩根手指同時觸摸一個view,那麼view只會調用一次touchesBegan:withEvent:方法,touches參數中裝著2個UITouch對象
4.如果這兩根手指一前一後分開觸摸同一個view,那麼view會分別調用2次touchesBegan:withEvent:方法,並且每次調用時的touches參數中只包含一個UITouch對象
5.根據touches中UITouch的個數可以判斷出是單點觸摸還是多點觸摸
6.如果沒有重寫這些touches方法,默認做法是將事件順著響應者鏈條向上傳遞,將事件交給上一個(往底層方向)響應者進行處理
7.在重寫touches方法時,如果調用了[super touches…];就會將事件順著響應者鏈條往上(往底層方向)傳遞,傳遞給上一個響應者
1.響應者鏈條:是由多個響應者對象連接起來的鏈條
2.響應者鏈條作用:能很清楚的看見每個響應者之間的聯系,並且可以讓一個事件多個對象處理
3.響應者對象:能處理事件的對象,如 :Application 、window 、view controller、view、child view
4.如何判斷上一個(往底層方向)響應者
(1). 如果當前這個view是控制器的view,那麼控制器就是上一個響應者
(2). 如果當前這個view不是控制器的view,那麼其父控件就是上一個響應者
5.響應者鏈的事件傳遞過程
(1). 如果view的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父視圖
(2). 在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理
(3). 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
(4). 如果UIApplication也不能處理該事件或消息,則將其丟棄
1.尋找最合適的響應者是從最底層往上找的,如 window —> view —> child View
2.響應者鏈是從最上層往底層傳遞的,如 child view —> view —> view controller —> window —> Application
3.監聽一個view上面的觸摸事件
(1). 自定義一個view
(2). 實現view的touches方法,在方法內部實現具體處理代碼
4.storyboard上的控件,在沒有設置控件的class時不能從storyboard往代碼上拖線,但是通過手寫代碼加上IBOutlet或Action,就可以從代碼向storyboard上拖線
5.關於事件的注意點
(1). 系統事件就交給代理處理,不是就交給窗口處理
(2). 如果父控件不能接收觸摸事件,那麼子控件就不可能接收到觸摸事件
(3). 如果父控件隱藏了,它的子控件也看不到了
(4). 如果設置父控件的透明度,子控件也會受到影響
(5). 調試bug時,當一個控件不能接收事件,看看他的父控件能不能接收事件
(6). 子控件超出父控件是可以顯示的,但超出的部分一般不做事件處理
6.寫代碼的時候按功能抽取方法