iOS事件傳遞及響應(yīng)鏈的探究

在iOS中,用戶與APP進行交互,會產(chǎn)生很多事件,這些事件是如何產(chǎn)生,響應(yīng)的鏈條又是怎樣傳遞的,本文將會進行一番探究。

事件分類

對于iOS用戶來說,他們操作設(shè)備的方式主要有三種:觸摸屏幕、晃動設(shè)備、遠程控制設(shè)備。對應(yīng)的事件類型有以下三種:

  • 觸屏事件(Touch Event)
  • 運動事件(Motion Event)
  • 遠端控制事件(Remote-Control Event)
    本文以常見的觸屏事件(Touch Event)來對事件傳遞以及響應(yīng)鏈來進行探究。

響應(yīng)

點擊、搖動、滑動、旋轉(zhuǎn)等會被系統(tǒng)封裝成UIEvent,放到事件隊列里等待UIApplication去取,然后尋找響應(yīng)者,找到對應(yīng)的方法并執(zhí)行的過程就是響應(yīng)。

響應(yīng)者

在iOS中,響應(yīng)者是能響應(yīng)事件的UIResponder子類的對象,如UIButtonUIView、UIViewController等。

響應(yīng)鏈

響應(yīng)鏈是由鏈接在一起的響應(yīng)者(UIResponse子類)組成的。默認情況下,響應(yīng)鏈是由第一響應(yīng)者,到application對象以及中間所有響應(yīng)者一起組成的。


響應(yīng)鏈

事件的產(chǎn)生

發(fā)生觸摸事件后,系統(tǒng)會將該事件加入到一個由UIApplication管理的事件隊列中,為什么是隊列而不是棧?因為隊列的特點是FIFO,即先進先出,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊列。

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

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

找到合適的視圖控件后,就會調(diào)用視圖控件的touches方法來作具體的事件處理。

事件傳遞&響應(yīng)流程

圖片來源于網(wǎng)絡(luò)

UITouch會給gestureRecognizers最優(yōu)響應(yīng)者也就是hitTestView發(fā)送消息

默認view會走其touchBegan:withEvent:等方法,當gestureRecognizers找到識別的gestureRecognizer后,將會獨自占有該touch,即會調(diào)用其他gestureRecognizerhitTestViewtouchCancelled:withEvent:方法,并且它們不再收到該touch事件,也就不會走響應(yīng)鏈流程。當該事件響應(yīng)完畢,主線程的Runloop開始睡眠,等待下一個事件。

不管視圖能不能處理事件,只要點擊了視圖就都會產(chǎn)生事件,關(guān)鍵在于該事件最終是由誰來處理!

尋找響應(yīng)者

尋找響應(yīng)者依靠這兩個方法:

// 返回最佳響應(yīng)者
open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// 判斷點有沒有在返回的視圖范圍內(nèi)
open func point(inside point: CGPoint, with event: UIEvent?) -> Bool
  • 當觸摸屏幕之后,系統(tǒng)會利用Runloop將事件加入到UIApplication的任務(wù)隊列中;
  • UIApplication分發(fā)觸摸事件到UIWindow,然后UIWindow依次向下分發(fā)給UIView;
  • UIView調(diào)用hitTest:withEvent:方法看看自己能否處理事件,以及觸摸點是否在自己上面;
  • 如果滿足條件,就遍歷UIView上的子控件。重復(fù)上面的動作。
  • 直到找到最頂層的一個滿足條件(既能處理觸摸事件,觸摸點又在上面)的子控件,此子控件就是我們需要找到的第一響應(yīng)者。

hitTest:withEvent:方法的偽代碼

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.userInteractionEnabled || !self.hidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            CGPoint subPoint = [subView convertPoint:point fromView:self];
            UIView *bestView = [subView hitTest:subPoint withEvent:event];
            if (bestView) {
                return bestView;
            }
        }
        return self;
    }
    return nil;
}

事件的響應(yīng)流程

找到第一響應(yīng)者后,需要逆著尋找第一響應(yīng)者的方向(從第一響應(yīng)者->UIApplication)來響應(yīng)事件。

流程如下:

  • 首先通過hitTest:withEvent:確定第一響應(yīng)者,以及相應(yīng)的響應(yīng)鏈;
  • 判斷第一響應(yīng)者能否響應(yīng)事件,如果第一響應(yīng)者能進行響應(yīng),那么響應(yīng)鏈的傳遞終止。如果第一響應(yīng)者不能響應(yīng)則將事件傳遞給nextResponder也就是通常的superview進行事件響應(yīng);
  • 如果事件繼續(xù)上報至UIWindow并且無法響應(yīng),它將會把事件繼續(xù)上報給UIApplication;
  • 如果事件繼續(xù)上報至UIApplication并且也無法響應(yīng),將會將事件上報給其delegate
  • 如果最終事件依舊未被響應(yīng)則會被系統(tǒng)拋棄;

需要注意的地方

第一響應(yīng)者對event的具體處理,是在事件響應(yīng)的過程中進行判定的。
hidden = YES視圖被隱藏,不接受響應(yīng)事件
userInteractionEnabled = NO,不接受響應(yīng)事件
alpha <= 0.01,透明視圖不接收響應(yīng)事件
子視圖超出父視圖范圍,不接收響應(yīng)事件
需響應(yīng)視圖被其他視圖蓋住,不接收響應(yīng)事件
是否重寫了其父視圖以及自身的hitTest方法
是否重寫了其父視圖以及自身的pointInside方法

實例

一、B是A的子視圖,要求觸摸B,B會相應(yīng)事件,觸摸A,不會響應(yīng)事件

image.png

我們只需要在A中,加入如下代碼:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard let result = super.hitTest(point, with: event) else  { return nil }
    if result == self {
        return nil
    }
    return result
}

二、B是A的子視圖,但是B有部分在A的外面,現(xiàn)在要點擊在外面的那部分,能夠響應(yīng)事件

image.png

方法1:

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    let testView = self.viewWithTag(10001)! // 獲取B
    if testView.frame.contains(point) {
        return true
    }
    return false
}

方法2:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let result = super.hitTest(point, with: event)
    if result == nil {
        let testView = self.viewWithTag(10001)!
        let newPoint = testView.convert(point, from: self) // 轉(zhuǎn)換坐標到子視圖
        if testView.bounds.contains(newPoint) {
            return testView
        }
    }
    return result
}

參考資料:

?著作權(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ù)。

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