iOS事件傳遞及響應(yīng)者鏈條

這里, 將帶你解析"從你的手指觸摸屏幕, 到App作出響應(yīng)"這段歷程中究竟發(fā)生了什么事.

什么是事件?

三大事件.png

iOS中事件分為3大類 : 觸摸事件, 加速計(jì)事件和遠(yuǎn)程控制事件.

當(dāng)你的手指在手機(jī)屏幕上觸摸時(shí), 產(chǎn)生了一個(gè)事件.
當(dāng)你拿起手機(jī)搖一搖時(shí), 產(chǎn)生了一個(gè)事件...

那么, 什么對(duì)象能夠響應(yīng), 處理這些事件?

響應(yīng)者

只有繼承自UIResponder的對(duì)象才能接收并處理事件, 我們把之類對(duì)象稱之為'響應(yīng)者'. UIApplication, UIViewController, UIView都繼承自UIResponder, 因此他們都可以接收處理事件.

UIResponder內(nèi)部提供處理事件的方法有 :

觸摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

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

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

除了以上方法, 現(xiàn)在還有一個(gè)類, UIGestureRecognizer, 它是一個(gè)抽象類,使用它的子類能幫助我們輕松識(shí)別view上的各種手勢(shì).

UITapGestureRecognizer // 敲擊
UIPinchGestureRecognizer // 捏合,用于縮放
UIPanGestureRecognizer // 拖拽
UISwipeGestureRecognizer // 輕掃
UIRotationGestureRecognizer // 旋轉(zhuǎn)
UILongPressGestureRecognizer // 長(zhǎng)按

UITouch

當(dāng)你用一根手指觸摸屏幕時(shí), 會(huì)創(chuàng)建一個(gè)與之關(guān)聯(lián)的UITouch對(duì)象, 一個(gè)UITouch對(duì)象對(duì)應(yīng)一根手指. 在事件中可以根據(jù)NSSet中UITouch對(duì)象的數(shù)量得出此次觸摸事件是單指觸摸還是雙指多指等等.

UITouch幾個(gè)重要的屬性 :

// 觸摸產(chǎn)生時(shí)所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;

// 觸摸產(chǎn)生時(shí)所處的視圖
@property(nonatomic,readonly,retain) UIView *view;

// 短時(shí)間內(nèi)點(diǎn)按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點(diǎn)擊
@property(nonatomic,readonly) NSUInteger tapCount;

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

// 當(dāng)前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase phase;

UITouch的兩個(gè)方法 (可用于view的拖拽)

/* 
  返回值表示觸摸在view上的位置
  這里返回的位置是針對(duì)傳入的view的坐標(biāo)系(以view的左上角為原點(diǎn)(0, 0))
  調(diào)用時(shí)傳入的view參數(shù)為nil的話,返回的是觸摸點(diǎn)在UIWindow的位置
*/
- (CGPoint)locationInView:(UIView *)view;

// 該方法記錄了前一個(gè)觸摸點(diǎn)的位置
- (CGPoint)previousLocationInView:(UIView *)view;

UIEvent

每產(chǎn)生一個(gè)事件, 就對(duì)應(yīng)產(chǎn)生一個(gè)UIEvent. UIEvent記錄著該事件產(chǎn)生的時(shí)間, 事件的類型等等.

UIEvent幾個(gè)重要的屬性 :

// 事件類型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
// 事件產(chǎn)生的時(shí)間
@property(nonatomic,readonly) NSTimeInterval timestamp;

事件的產(chǎn)生與傳遞

觸摸4這個(gè)view

手機(jī)屏幕是一個(gè)2D的平面, 這上面有幾個(gè)view重疊在一起, 系統(tǒng)并不能分辨出你點(diǎn)擊的是哪一個(gè)view, 所以響應(yīng)你的觸摸事件的是這個(gè)應(yīng)用程序UIApplication, 而不是其中的一個(gè)UIView. 系統(tǒng)會(huì)將這次觸摸事件加入到由UIApplication管理的一個(gè)隊(duì)列中, 每次從中取出事件分發(fā)下去處理. 一般先發(fā)給keyWindow.

keyWindow會(huì)在視圖的層級(jí)結(jié)構(gòu)中找到一個(gè)最合適的控件來(lái)處理觸摸事件.

由于觸摸事件的傳遞方向是由父控件傳遞到子控件, 那何為最合適呢?

  • 自己能響應(yīng)觸摸事件
  • 觸摸點(diǎn)在自己身上
  • 從后往前遍歷子控件, 重復(fù)上兩步
  • 如果沒(méi)有符合條件的子控件, 那么就自己最合適處理

根據(jù)上圖來(lái)說(shuō)明的話

// 點(diǎn)擊了綠色的view:
UIApplication -> UIWindow -> 白色 -> 綠色
// 點(diǎn)擊了藍(lán)色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍(lán)色
// 點(diǎn)擊了黃色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 藍(lán)色 -> 黃色

兩個(gè)系統(tǒng)在尋找最合適view的時(shí)候使用到的方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
當(dāng)事件傳遞給控件的時(shí)候, 就會(huì)調(diào)用該方法, 去尋找最合適的view并返回

底層實(shí)現(xiàn)如下 : 
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    
    // 1.判斷當(dāng)前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

    // 2. 判斷點(diǎn)在不在當(dāng)前控件
    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];
        
        // 把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
        CGPoint childP = [self convertPoint:point toView:childView];
        
        UIView *fitView = [childView hitTest:childP withEvent:event];
        
        if (fitView) { // 尋找到最合適的view
            return fitView;
        }
    }
    
    // 循環(huán)結(jié)束,表示沒(méi)有比自己更合適的view
    return self;
    
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
該方法判斷觸摸點(diǎn)是否在控件身上, 是則返回YES, 否則返回NO

作用

可以使用以上兩個(gè)方法做到

  • 指鹿為馬(明明點(diǎn)擊的是B視圖, 卻由A視圖來(lái)響應(yīng)事件)
  • 穿透某控件點(diǎn)擊被覆蓋的下一層控件
  • 讓父控件frame之外的子控件響應(yīng)觸摸事件

響應(yīng)者鏈條

當(dāng)找到最合適的響應(yīng)者之后, 便會(huì)調(diào)用控件相應(yīng)的touches方法來(lái)作具體處理. 然而這些方法默認(rèn)是不處理, 并將該事件隨著響應(yīng)者鏈條往回傳遞, 交給上一個(gè)響應(yīng)者來(lái)處理. (即調(diào)用super的touches方法)

響應(yīng)者鏈條.png

很詭異吧, 好不容易找到了你, 你卻跟老子說(shuō)你不干活, 給前面找你的人干?

其實(shí)蘋果這樣設(shè)計(jì)是為了讓一個(gè)事件能夠被多個(gè)對(duì)象處理!

那么問(wèn)題來(lái)了~ 誰(shuí)是上一個(gè)響應(yīng)者? 如圖所示 :

1. 如果view的控制器存在,就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父視圖
2. 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件傳遞給window對(duì)象進(jìn)行處理
3. 如果window對(duì)象也不處理,則其將事件或消息傳遞給UIApplication對(duì)象
4. 如果UIApplication也不能處理該事件或消息,則將其丟棄

有趣的是,當(dāng)調(diào)用UIControl的addTarget:action:forControlEvents:時(shí),如果target設(shè)為nil,系統(tǒng)則會(huì)從UIControl本身開始沿著響應(yīng)鏈往上找,直到找到有一個(gè)響應(yīng)者響應(yīng)了該事件為止,否則該事件則被丟棄。

Coffee time!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容