一、事件對象
1. UIResponder 響應(yīng)者對象
簡介
- 只有繼承了 UIResponder 對象才能 接收 并 處理 事件
UIApplication、UIViewController、UIView 都繼承自 UIResponder
處理事件的方法「通過覆蓋以下方法實現(xiàn)對事件的處理」
觸摸事件
- 有 Began/ Moved/ Ended/ Cancelled 四種「針對 多根 或 一根手指」的事件方法
- 默認的這四種觸摸事件 只是將事件傳遞給上一個響應(yīng)著「默認上一個響應(yīng)者是父控件」
- 根據(jù) touches 中 UITouch 的個數(shù)可以判斷出是單點觸摸還是多點觸摸
如果兩根手指同時觸摸 1 個 view,那么 view 只會調(diào)用一次 touchesBegan:withEvent: 方法,touches 參數(shù)中裝著 2 個UITouch 對象
如果這兩根手指一前一后分開觸摸同一個 view,那么 view 會分別調(diào)用 2 次 touchesBegan:withEvent: 方法,并且每次調(diào)用時的 touches 參數(shù)中只包含 1 個 UITouch 對象 - 部分方法示例:
// 一根或者多根手指開始觸摸view,系統(tǒng)會自動調(diào)用view的下面方法
// Began/ Moved/ Ended/ Cancelled,4 種方法 都是 同一個 event 參數(shù)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 事件 Cancelled 用于 觸摸結(jié)束前某個系統(tǒng)事件打斷觸摸過程,系統(tǒng)會自動調(diào)用 View下面的 Cancelled 方法
// touches 存放的都是 UITouch 對象
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速事件
- 有 Began/ Ended/ Cancelled 三種,例如:
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
2. UITouch 對象
簡介
保存手指觸摸的相關(guān)信息:1 個手指 對應(yīng) 1 個 UITouch 對象
手指觸摸屏幕時,創(chuàng)建 1 個與手指關(guān)聯(lián)的 UITouch 對象
手指移動時,系統(tǒng)會更新對應(yīng)的同 1 個 UITouch 對象
手指離開屏幕時,系統(tǒng)會銷毀相應(yīng)的 UITouch 對象
注:iPhone 開發(fā)時,要避免雙擊事件
屬性
@property(nonatomic,readonly,retain) UIWindow *window; // 觸摸產(chǎn)生時所處的窗口
@property(nonatomic,readonly,retain) UIView *view; // 觸摸產(chǎn)生時所處的視圖
@property(nonatomic,readonly) NSUInteger tapCount; // 短時間內(nèi)點按屏幕的次數(shù),可用來判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSTimeInterval timestamp;// 記錄了觸摸事件產(chǎn)生或變化時的時間,單位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 當前觸摸事件所處的狀態(tài)
方法
// 返回值是 在 View 的坐標系上 當前觸摸的位置「中心點的位置」
// 調(diào)用時如果傳入 View 參數(shù)為 nil,則 返回 相對于 UIWindow 的位置
- (CGPoint)locationInView:(UIView *)view;
// 記錄 前一個 觸摸位置
- (CGPoint)previousLocationInView:(UIView *)view;
3. UIEvent 對象
簡介
- 每 1 個事件就會創(chuàng)建 1 個 UIEvent 對象
- UIEvent 對象用于記錄事件產(chǎn)生的 時刻 和 類型
屬性
@property(nonatomic,readonly) UIEventType type; // 事件類型
@property(nonatomic,readonly) UIEventSubtype subtype;
@property(nonatomic,readonly) NSTimeInterval timestamp; // 事件產(chǎn)生的時間
UIEvent 還提供了相應(yīng)的方法可以獲得在某個 view 上面的觸摸對象「UITouch」
4. 示例:UIView 的拖拽
// 當手指在 view 上移動的時候「在 UIView 中實現(xiàn)的方法」
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// 獲取UITouch對象
UITouch *touch = [touches anyObject];
// 獲取當前點
CGPoint curP = [touch locationInView:self];
// 獲取上一個點
CGPoint preP = [touch previousLocationInView:self];
// 獲取x軸偏移量
CGFloat offsetX = curP.x - preP.x;
// 獲取y軸偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改view的位置(frame,center,transform)
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
二、找到發(fā)生事件的最合適控件
I. 事件的產(chǎn)生過程
- 系統(tǒng)將發(fā)生的事件加入到 UIApplication 管理的 事件隊列
- UIApplication 取出隊列的首個事件,將事件分發(fā)下去處理,「通常先發(fā)送事件給應(yīng)用程序的主窗口」
- 在 主窗口在視圖結(jié)構(gòu)層次中 找到最適合的視圖來處理
II. 找到最合適的控件的步驟「HitTest」
找到最合適的控件的步驟,也是 HitTest 的系統(tǒng)實現(xiàn)方法
- 視圖能否 接收事件「是否繼承自 UIResponder」
- 觸摸點是否 在視圖上面「
pointInside方法返回 YES」 - 從后往前遍歷子控件,重復(fù)前面兩個步驟
<span style="color:red;">這里說的子控件指:在添加父控件類中添加的子控件,兩個控件的類不一定是繼承關(guān)系</span>
代碼模擬
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當前控件能否接收事件,這里 self 指繼承自 UIView 的自定義 view
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) 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];
// 把當前控件上的坐標系轉(zhuǎn)換成子控件上的坐標系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,但表示沒有比自己更合適的view
return self;
}
III. 注意
在 主窗口在視圖結(jié)構(gòu)層次中,事件從 父控件 傳遞到 子控件「先由上往下 傳遞」
若 父控件不能接收觸摸事件,子控件更不能,則 這個父控件的父控件會處理事件
若 子控件不能接收觸摸事件,事件由子控件 可以接收事件的父控件 處理
父控件之外的子控件,能顯示,不能處理事件
UIView 的方法
// hitTest 的作用:用來尋找最合適的 View
// 調(diào)用時刻:當事件傳遞給控件的時候
// 可以通過更改此函數(shù)的返回值,來人為的選取 處理事件的控件
// point:當前的觸摸點,point 這個點的坐標系就是方法調(diào)用者的坐標系
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// 作用:判斷當前觸摸點在不在方法調(diào)用者「控件」上
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
三、事件的處理
響應(yīng)者鏈條
- 由多個響應(yīng)者對象連接起來的鏈條
- 能很清楚的看見每個響應(yīng)者之間的聯(lián)系,并且可以讓一個事件多個對象處理「默認一個事件由一個對象處理」
響應(yīng)順序
- 若 View 的控制器存在,傳遞給控制器;若不存在,傳給 View 的父視圖
- 若視圖層次結(jié)構(gòu)的最頂級視圖不能處理,則 傳遞給 Window 對象進行處理
- 若 Window 對象不能處理,則 傳遞給 UIApplication 對象
- 若 UIApplication 對象不能處理,則 丟棄事件
I. 不接受觸摸事件的情況
- 不接收用戶交互
userInteractionEnabled = NO; - 隱藏
hidden = YES;「父控件隱藏,子控件也會隱藏」 - 透明
alpha = 0.0 ~ 0.01「父控件透明度變化,子控件透明度也會做相應(yīng)的變化」
注:UIImageView 的 userInteractionEnable 默認是 NO,因此 UIImageView默認不能接收觸摸事件
II. 事件處理順序
事件處理順序,也是 touches 方法 默認處理順序
- 調(diào)用最合適控件的
touchesBegan/Moved/Ended/Cancelled...方法
這些 touches 方法 默認將事件順著響應(yīng)鏈條向上傳遞,交給上一個響應(yīng)者處理 - 如果最合適控件調(diào)用了
[super touchesBegan/Moved/Ended/Cancelled...];
就會將事件順 響應(yīng)者鏈條 向上傳遞,傳遞給上一個響應(yīng)者處理事件「由下往上 處理」 - 調(diào)用響應(yīng)者的
touchesBegan/Moved/Ended/Cancelled...方法
找到上一個響應(yīng)者的方法
- 當前 View 是 控制器的 View,則上一個響應(yīng)者為 控制器
- 當前 View 不是 控制器的 View,則上一個響應(yīng)者為 父控件
四、手勢識別
沒有手勢識別時,監(jiān)聽一個 View 上面的觸摸事件
- 自定義一個 View
- 實現(xiàn) View 的
touchesBegan/Moved/Ended/Cancelled...方法,在方法內(nèi)部實現(xiàn)具體處理代碼
不用手勢識別的缺點
- 必須得自定義 View
- 由于是在 View 內(nèi)部的 touches 方法中監(jiān)聽觸摸事件,因此默認情況下,無法讓其他外界對象監(jiān)聽 View 的觸摸事件
- 不易區(qū)分用戶的具體手勢行為
1. UIGestureRecognizer「抽象類」
簡介
- 為了完成手勢識別,必須借助于手勢識別器 UIGestureRecognizer
- 定義了所有手勢的基本行為,使用它的子類才能處理具體的手勢
- UIGestureRecognizer 的子類有:
UITapGestureRecognizer // 敲擊
UIPinchGestureRecognizer // 捏合,用于縮放
UIPanGestureRecognizer // 拖拽
UISwipeGestureRecognizer // 輕掃
UIRotationGestureRecognizer // 旋轉(zhuǎn)
UILongPressGestureRecognizer // 長按
2. 手勢識別的狀態(tài)
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 沒有觸摸事件發(fā)生,所有手勢識別的默認狀態(tài)
UIGestureRecognizerStateBegan, // 一個手勢已經(jīng)開始但尚未改變或者完成時
UIGestureRecognizerStateChanged, // 手勢狀態(tài)改變
UIGestureRecognizerStateEnded, // 手勢完成
UIGestureRecognizerStateCancelled, // 手勢取消,恢復(fù)至Possible狀態(tài)
UIGestureRecognizerStateFailed, // 手勢失敗,恢復(fù)至Possible狀態(tài)
// 識別到手勢識別
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
3. 使用步驟
每一個手勢識別器的用法都差不多,比如 UITapGestureRecognizer 的使用步驟如下
// 1. 創(chuàng)建手勢識別器對象
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
// 2. 設(shè)置手勢識別器對象的具體屬性
// 連續(xù)敲擊 2 次
tap.numberOfTapsRequired = 2;
// 需要 2 根手指一起敲擊
tap.numberOfTouchesRequired = 2;
// 3. 「如果必要」手勢是可以設(shè)置代理的,要遵守 UIGestureRecognizerDelegate 協(xié)議
tap.delegate = self;
// 4. 添加手勢識別器到對應(yīng)的view上
[self.iconView addGestureRecognizer:tap];
// 5. 監(jiān)聽手勢的觸發(fā)
[tap addTarget:self action:@selector(tapIconView:)];
手勢的常用代理方法
// 是否允許開始觸發(fā)手勢
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
// 是否允許同時支持多個手勢,默認是不支持多個手勢「Yes 表示支持多個手勢」
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
// 是否允許接收手指的觸摸點「可以控制一個 View 只有部分能夠點擊」
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch