iOS 時間響應(yīng) 和 手勢識別

一、事件對象

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)生過程

  1. 系統(tǒng)將發(fā)生的事件加入到 UIApplication 管理的 事件隊列
  2. UIApplication 取出隊列的首個事件,將事件分發(fā)下去處理,「通常先發(fā)送事件給應(yīng)用程序的主窗口」
  3. 主窗口在視圖結(jié)構(gòu)層次中 找到最適合的視圖來處理

II. 找到最合適的控件的步驟「HitTest」

找到最合適的控件的步驟,也是 HitTest 的系統(tǒng)實現(xiàn)方法

  1. 視圖能否 接收事件「是否繼承自 UIResponder」
  2. 觸摸點是否 在視圖上面「pointInside 方法返回 YES」
  3. 從后往前遍歷子控件,重復(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. 不接受觸摸事件的情況

  1. 不接收用戶交互 userInteractionEnabled = NO;
  2. 隱藏 hidden = YES;「父控件隱藏,子控件也會隱藏」
  3. 透明 alpha = 0.0 ~ 0.01「父控件透明度變化,子控件透明度也會做相應(yīng)的變化」

注:UIImageView 的 userInteractionEnable 默認是 NO,因此 UIImageView默認不能接收觸摸事件

II. 事件處理順序

事件處理順序,也是 touches 方法 默認處理順序

  1. 調(diào)用最合適控件touchesBegan/Moved/Ended/Cancelled... 方法
    這些 touches 方法 默認將事件順著響應(yīng)鏈條向上傳遞,交給上一個響應(yīng)者處理
  2. 如果最合適控件調(diào)用了 [super touchesBegan/Moved/Ended/Cancelled...];
    就會將事件順 響應(yīng)者鏈條 向上傳遞,傳遞給上一個響應(yīng)者處理事件「由下往上 處理」
  3. 調(diào)用響應(yīng)者的 touchesBegan/Moved/Ended/Cancelled... 方法

找到上一個響應(yīng)者的方法

  1. 當前 View 控制器的 View,則上一個響應(yīng)者為 控制器
  2. 當前 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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容