iOS開發——UI進階篇(十二)事件處理,觸摸事件,UITouch,UIEvent,響應者鏈條,手勢識別


觸摸事件
在用戶使用app過程中,會產生各種各樣的事件

一、iOS中的事件可以分為3大類型


觸摸事件
加速計事件
遠程控制事件

響應者對象
在iOS中不是任何對象都能處理事件,只有繼承了UIResponder的對象才能接收並處理事件。我們稱之為“響應者對象”

UIApplication、UIViewController、UIView都繼承自UIResponder,因此它們都是響應者對象,都能夠接收並處理事件


二、UIResponder


UIResponder內部提供了以下方法來處理事件
觸摸事件
- (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;

加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

遠程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;


三、UIView的觸摸事件處理


UIView是UIResponder的子類,可以覆蓋下列4個方法處理不同的觸摸事件
一根或者多根手指開始觸摸view,系統會自動調用view的下面方法

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

一根或者多根手指在view上移動,系統會自動調用view的下面方法(隨着手指的移動,會持續調用該方法)

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

一根或者多根手指離開view,系統會自動調用view的下面方法

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

觸摸結束前,某個系統事件(例如電話呼入)會打斷觸摸過程,系統會自動調用view的下面方法

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event


提示:touches中存放的都是UITouch對象


四、UITouch


當用戶用一根手指觸摸屏幕時,會創建一個與手指相關聯的UITouch對象

一根手指對應一個UITouch對象

1、UITouch的作用
保存着跟手指相關的信息,比如觸摸的位置、時間、階段

當手指移動時,系統會更新同一個UITouch對象,使之能夠一直保存該手指在的觸摸位置

當手指離開屏幕時,系統會銷毀相應的UITouch對象

提示:iPhone開發中,要避免使用雙擊事件!

 

2、UITouch的屬性

觸摸產生時所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;

觸摸產生時所處的視圖
@property(nonatomic,readonly,retain) UIView *view;

短時間內點按屏幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSUInteger tapCount;

記錄了觸摸事件產生或變化時的時間,單位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;

當前觸摸事件所處的狀態
@property(nonatomic,readonly) UITouchPhase phase;

 

3、UITouch的方法

- (CGPoint)locationInView:(UIView *)view;

返回值表示觸摸在view上的位置
這里返回的位置是針對view的坐標系的(以view的左上角為原點(0, 0))
調用時傳入的view參數為nil的話,返回的是觸摸點在UIWindow的位置

- (CGPoint)previousLocationInView:(UIView *)view;

該方法記錄了前一個觸摸點的位置


五、UIEvent


每產生一個事件,就會產生一個UIEvent對象

UIEvent:稱為事件對象,記錄事件產生的時刻和類型

 

2、常見屬性

事件類型

@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;

事件產生的時間

@property(nonatomic,readonly) NSTimeInterval timestamp;

UIEvent還提供了相應的方法可以獲得在某個view上面的觸摸對象(UITouch)

 

3、touches和event參數
一次完整的觸摸過程,會經歷3個狀態:

觸摸開始:- (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

 

4個觸摸事件處理方法中,都有NSSet *touches和UIEvent *event兩個參數
一次完整的觸摸過程中,只會產生一個事件對象,4個觸摸方法都是同一個event參數

如果兩根手指同時觸摸一個view,那么view只會調用一次touchesBegan:withEvent:方法,touches參數中裝着2個UITouch對象

如果這兩根手指一前一后分開觸摸同一個view,那么view會分別調用2次touchesBegan:withEvent:方法,並且每次調用時的touches參數中只包含一個UITouch對象

根據touches中UITouch的個數可以判斷出是單點觸摸還是多點觸摸


六、事件的產生和傳遞


1、發生觸摸事件后,系統會將該事件加入到一個由UIApplication管理的事件隊列中

UIApplication會從事件隊列中取出最前面的事件,並將事件分發下去以便處理,通常,先發送事件給應用程序的主窗口(keyWindow)

主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件,這也是整個事件處理過程的第一步

找到合適的視圖控件后,就會調用視圖控件的touches方法來作具體的事件處理
touchesBegan…
touchesMoved…
touchedEnded…

 

2、事件傳遞示例


觸摸事件的傳遞是從父控件傳遞到子控件
點擊了綠色的view:
UIApplication -> UIWindow -> 白色 -> 綠色
點擊了藍色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色
點擊了黃色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍色 -> 黃色

如果父控件不能接收觸摸事件,那么子控件就不可能接收到觸摸事件(掌握)

如何找到最合適的控件來處理事件?
1)自己是否能接收觸摸事件?
2)觸摸點是否在自己身上?
3)從后往前遍歷子控件數組,重復前面的兩個步驟
4)如果沒有符合條件的子控件,那么就自己最適合處理
hitTest方法內部原理(模擬蘋果)

// 什么時候調用:只要事件一傳遞給一個控件就會調用
// 作用:尋找最合適的view給你
// UIApplication -> [UIWindow hitTest:withEvent:]尋找最合適的view告訴系統
// point:當前手指觸摸的點
// point:是方法調用者坐標系上的點
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//   UIView *fitView = [super hitTest:point withEvent: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;
    
    // 0 1
    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) {// 如果能找到最合適的view
            return fitView;
        }
    }
    // 4.沒有比自己更合適的view
    return self;
}

 注意這個方法,可以主動攔截事件的傳遞

// 作用:判斷下傳入過來的點在不在方法調用者的坐標系上
// point:是方法調用者坐標系上的點
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return NO;
}

如果重寫該方法直接返回no

那么就能成功在這一層攔截事件,也就是說事件傳遞到這一層就告訴你觸摸事件處理不了

 

3、UIView不接收觸摸事件的三種情況
1、不接收用戶交互
userInteractionEnabled = NO

2、隱藏
hidden = YES

3、透明
alpha = 0.0 ~ 0.01

提示:UIImageView的userInteractionEnabled默認就是NO,因此UIImageView以及它的子控件默認是不能接收觸摸事件的


七、響應者鏈條


1、觸摸事件處理的詳細過程
用戶點擊屏幕后產生的一個觸摸事件,經過一系列的傳遞過程后,會找到最合適的視圖控件來處理這個事件

找到最合適的視圖控件后,就會調用控件的touches方法來作具體的事件處理
touchesBegan…
touchesMoved…
touchedEnded…

這些touches方法的默認做法是將事件順着響應者鏈條向上傳遞,將事件交給上一個響應者進行處理

 

2、響應者鏈條示意圖


響應者鏈條:是由多個響應者對象連接起來的鏈條
作用:能很清楚的看見每個響應者之間的聯系,並且可以讓一個事件多個對象處理。
響應者對象:能處理事件的對象

 

3、事件傳遞的完整過程
  1> 先將事件對象由上往下傳遞(由父控件傳遞給子控件),找到最合適的控件來處理這個事件。

  2> 調用最合適控件的touches….方法

  3> 如果調用了[super touches….];就會將事件順着響應者鏈條往上傳遞,傳遞給上一個響應者

  4> 接着就會調用上一個響應者的touches….方法

 

4、如何判斷上一個響應者
  1> 如果當前這個view是控制器的view,那么控制器就是上一個響應者

  2> 如果當前這個view不是控制器的view,那么父控件就是上一個響應者

 

5、響應者鏈的事件傳遞過程
如果view的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父視圖
在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理
如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
如果UIApplication也不能處理該事件或消息,則將其丟棄

 

八、手勢識別


1、監聽觸摸事件的做法
如果想監聽一個view上面的觸摸事件,之前的做法是
  自定義一個view
  實現view的touches方法,在方法內部實現具體處理代碼

通過touches方法監聽view觸摸事件,有很明顯的幾個缺點
  必須得自定義view
  由於是在view內部的touches方法中監聽觸摸事件,因此默認情況下,無法讓其他外界對象監聽view的觸摸事件
  不容易區分用戶的具體手勢行為

iOS 3.2之后,蘋果推出了手勢識別功能(Gesture Recognizer),在觸摸事件處理方面,大大簡化了開發者的開發難度

 

2、UIGestureRecognizer
為了完成手勢識別,必須借助於手勢識別器----UIGestureRecognizer

利用UIGestureRecognizer,能輕松識別用戶在某個view上面做的一些常見手勢

UIGestureRecognizer是一個抽象類,定義了所有手勢的基本行為,使用它的子類才能處理具體的手勢

UITapGestureRecognizer(敲擊)
UIPinchGestureRecognizer(捏合,用於縮放)
UIPanGestureRecognizer(拖拽)
UISwipeGestureRecognizer(輕掃)
UIRotationGestureRecognizer(旋轉)
UILongPressGestureRecognizer(長按)

 

2.1、UITapGestureRecognizer(點按手勢)
每一個手勢識別器的用法都差不多,比如UITapGestureRecognizer的使用步驟如下

創建手勢識別器對象
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];

設置手勢識別器對象的具體屬性
// 連續敲擊2次
tap.numberOfTapsRequired = 2;
// 需要2根手指一起敲擊
tap.numberOfTouchesRequired = 2;

添加手勢識別器到對應的view上
[self.iconView addGestureRecognizer:tap];

監聽手勢的觸發
[tap addTarget:self action:@selector(tapIconView:)];

 

2.2、UILongPressGestureRecognizer(長按手勢)

- (void)setUpLongPress
{
    // 長按手勢
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    
    [_imageView addGestureRecognizer:longPress];

}

// 什么時候調用:長按的時候調用,而且只要手指不離開,拖動的時候會一直調用,手指抬起的時候也會調用
- (void)longPress:(UILongPressGestureRecognizer *)longPress
{
    // 注意:在以后開發中,長按手勢一般需要做判斷
    if (longPress.state == UIGestureRecognizerStateEnded) {
        
        NSLog(@"%s",__func__);
    }
}

 

2.3、UISwipeGestureRecognizer(輕掃)

- (void)setUpSwipe
{
    // 一個手勢只能對應一個方向
    // 設置輕掃的方向往右(不設置的話默認為右)
    UISwipeGestureRecognizer *swipeR = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    
    // 設置輕掃的方向
    swipeR.direction = UISwipeGestureRecognizerDirectionRight;
    
    [_imageView addGestureRecognizer:swipeR];
    
    
    
    // 設置輕掃的方向往左
    UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    
    // 設置輕掃的方向
    swipe.direction = UISwipeGestureRecognizerDirectionLeft;
    
    [_imageView addGestureRecognizer:swipe];
}

- (void)swipe:(UISwipeGestureRecognizer *)swipe
{
    if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {
        // 往右邊輕掃 
    }else{
        // 往左邊輕掃
        NSLog(@"%s左邊",__func__);
    }
}

 2.4 UIRotationGestureRecognizer(旋轉手勢)

#pragma mark - 添加旋轉手勢
- (void)setUpRotation
{
    // 旋轉
    UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotation:)];
    
    rotation.delegate = self;
    
    [_imageView addGestureRecognizer:rotation];
}
- (void)rotation:(UIRotationGestureRecognizer *)rotation
{
 
    // 獲取的角度是相對於最原始的角度
    NSLog(@"%f",rotation.rotation);
    
    // 旋轉圖片
    _imageView.transform = CGAffineTransformRotate(_imageView.transform, rotation.rotation);
    
    // 復位,只要想相對於上一次旋轉就復位
    rotation.rotation = 0;
    
}

2.5 UIPinchGestureRecognizer(捏合)

#pragma mark - 添加捏合手勢
- (void)setUpPinch
{
    // 捏合
    UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
    
    pinch.delegate = self;
    
    [_imageView addGestureRecognizer:pinch];
}

- (void)pinch:(UIPinchGestureRecognizer *)pinch
{
    
    _imageView.transform = CGAffineTransformScale(_imageView.transform, pinch.scale, pinch.scale);
    
    // 復位
    pinch.scale = 1;
}

2.6  UIPanGestureRecognizer(拖拽)

- (void)viewDidLoad {
    [super viewDidLoad];

    // 拖拽手勢
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
                                   initWithTarget:self action:@selector(pan:)];
    
    [_imageView addGestureRecognizer:pan];
}

- (void)pan:(UIPanGestureRecognizer *)pan
{
    
    // 獲取手指平移的偏移量
    CGPoint transP = [pan translationInView:_imageView];
    
    _imageView.transform = CGAffineTransformTranslate(_imageView.transform, transP.x, transP.y);

    // 復位
    [pan setTranslation:CGPointZero inView:_imageView];
}

如果想同時擁有多種手勢,可實現以下方法

// Simultaneously:同時
// 是否同時支持多個手勢
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

 

3、手勢識別的狀態

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    // 沒有觸摸事件發生,所有手勢識別的默認狀態
    UIGestureRecognizerStatePossible,
    // 一個手勢已經開始但尚未改變或者完成時
    UIGestureRecognizerStateBegan,
    // 手勢狀態改變
    UIGestureRecognizerStateChanged,
    // 手勢完成
    UIGestureRecognizerStateEnded,
    // 手勢取消,恢復至Possible狀態
    UIGestureRecognizerStateCancelled, 
    // 手勢失敗,恢復至Possible狀態
    UIGestureRecognizerStateFailed,
    // 識別到手勢識別
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM