ios——事件傳遞與響應(yīng)者鏈

一、事件分類

事件是發(fā)送到應(yīng)用程序用于通知用戶操作的對(duì)象。 在iOS中,事件可以采取多種形式:多點(diǎn)觸摸事件,運(yùn)動(dòng)事件和用于控制多媒體的事件。 這最后一種類型的事件被稱為遙控事件或者遠(yuǎn)程控制事件,因?yàn)樗梢栽醋酝獠扛郊?。而在我們開發(fā)過程中最常用的就是多點(diǎn)觸摸事件。

二、事件傳遞

當(dāng)用戶生成的事件發(fā)生時(shí),UIKit創(chuàng)建一個(gè)包含處理事件所需信息的事件對(duì)象。 然后它將事件對(duì)象放置在活動(dòng)應(yīng)用程序的事件隊(duì)列中。 對(duì)于觸摸事件,該對(duì)象是在UIEvent對(duì)象中打包的一組觸摸(UIEvent中包含了所有UITouch信息)。 對(duì)于運(yùn)動(dòng)事件,事件對(duì)象因您使用的框架和您感興趣的運(yùn)動(dòng)事件類型而異。

事件沿著特定路徑傳遞,直到它被傳遞到可以處理它的對(duì)象。 首先,單例UIApplication對(duì)象從隊(duì)列的頂部獲取一個(gè)事件并分發(fā)處理。 通常,它將事件發(fā)送到應(yīng)用程序的key window對(duì)象,該對(duì)象將事件傳遞到初始對(duì)象(initial object)進(jìn)行處理。 初始對(duì)象取決于事件的類型。

  • 觸摸事件:對(duì)于觸摸事件,窗口對(duì)象首先嘗試將事件傳遞到發(fā)生觸摸的視圖。 該視圖稱為命中測(cè)試視圖(hit-test view)。 找到命中測(cè)試視圖(hit-test view)的過程稱為命中測(cè)試(hit-testing),這在Hit-Testing返回觸摸發(fā)生的視圖中描述。

  • 運(yùn)動(dòng)和遙控事件:對(duì)于這些事件,窗口對(duì)象將搖動(dòng)或遠(yuǎn)程控制事件發(fā)送到第一響應(yīng)者以進(jìn)行處理。 第一響應(yīng)者在響應(yīng)者鏈由響應(yīng)者對(duì)象組成中描述。

這些事件路徑的最終目標(biāo)是找到一個(gè)可以處理和響應(yīng)事件的對(duì)象。 因此,UIKit首先將事件發(fā)送到最適合處理事件的對(duì)象。 對(duì)于觸摸事件,該對(duì)象是命中測(cè)試視圖(hit-test view),對(duì)于其他事件,該對(duì)象是第一個(gè)響應(yīng)者。

{\large\text{作者:坤坤同學(xué) 鏈接:http://www.itdecent.cn/p/847432c2cb3b 來源:簡(jiǎn)書 著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。}}

三、命中測(cè)試

一根手指觸摸屏幕時(shí)會(huì)創(chuàng)建一個(gè)UITouch對(duì)象,最終生成UIEvent對(duì)象,并通過sendEvent:函數(shù)發(fā)送給UIWindowkeyWindow)。

  1. UIApplication接收到事件,將事件傳遞給keyWindow。
  2. keyWindow遍歷subViewshitTest:withEvent:方法,找到點(diǎn)擊區(qū)域內(nèi)合適的視圖來處理事件。
  3. UIView的子視圖也會(huì)遍歷其subViewshitTest:withEvent:方法,以此類推。
  4. 直到找到點(diǎn)擊區(qū)域內(nèi),且處于最上方的視圖,將視圖逐步返回給UIApplication。
  5. 在查找第一響應(yīng)者的過程中,已經(jīng)形成了一個(gè)響應(yīng)者鏈。
  6. 應(yīng)用程序會(huì)先調(diào)用第一響應(yīng)者處理事件。
  7. 如果第一響應(yīng)者不能處理事件,則調(diào)用其nextResponder方法,一直找響應(yīng)者鏈中能處理該事件的對(duì)象。
  8. 然后交給UIApplication后,最后交給UIApplicationDelegate,仍然沒有能處理該事件的對(duì)象,則該事件被廢棄。
  • 這里涉及兩條鏈:
    Hit-Testing鏈,由系統(tǒng)向命中view傳遞UIKit –> active app's event queue –> window –> root view –>......–>lowest view
    響應(yīng)鏈,由命中view向系統(tǒng)傳遞initial view –> super view –> .....–> view controller –> window –> Application –> AppDelegate

舉例說明:
1.如果點(diǎn)擊UITextField后其會(huì)成為第一響應(yīng)者。
2.如果textField未處理事件,則會(huì)將事件傳遞給下一級(jí)響應(yīng)者鏈,也就是其父視圖。
3.父視圖未處理事件則繼續(xù)向下傳遞,也就是UIViewControllerView。
4.如果控制器的View未處理事件,則會(huì)交給控制器處理。
5.控制器未處理則會(huì)交給UIWindow。
6.然后會(huì)交給UIApplication。
7.最后交給UIApplicationDelegate,如果其未處理則丟棄事件。

案例說明,假設(shè)用戶觸摸下圖中的View E。 iOS通過按照此順序檢查子視圖來查找命中測(cè)試視圖(hit-test view):

  1. 觸摸在View A的邊界內(nèi),因此它檢查子視圖View BView C.

  2. 觸摸不在View B的界限內(nèi),但它在View C的界限內(nèi),因此它檢查子視圖View DView E.

  3. 觸摸不在View D的界限內(nèi),但它在View E的界限內(nèi)。

View E是視圖層級(jí)中包含觸摸的最低的視圖,因此它成為命中測(cè)試視圖(hit-test view)。

//模擬代碼:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
        return nil;
    }
    
    BOOL inside = [self pointInside:point withEvent:event];
    if (inside) {
        NSArray *subViews = self.subviews;
        // 對(duì)子視圖從上向下找
        for (NSInteger i = subViews.count - 1; i >= 0; i--) {
            UIView *subView = subViews[i];
            CGPoint insidePoint = [self convertPoint:point toView:subView];
            UIView *hitView = [subView hitTest:insidePoint withEvent:event];
            if (hitView) {
                return hitView;
            }
        }
        return self;
    }
    return nil;
}
四、知識(shí)點(diǎn)應(yīng)用
  1. 調(diào)用hitTest,獲取到被點(diǎn)擊的視圖,也就是第一響應(yīng)者:
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
    想讓指定視圖來響應(yīng)事件,不再遍歷子視圖傳遞事件,可以通過重寫hitTest方法。

  2. hitTest方法內(nèi)部會(huì)通過調(diào)用pointInside,來判斷點(diǎn)擊區(qū)域是否在視圖上,是則返回YES,不是則返回NO
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
    通過重寫pointInside方法,可以將有效點(diǎn)擊區(qū)域擴(kuò)大。

  3. 另外,應(yīng)用程序通過響應(yīng)者來接收和處理事件(能夠響應(yīng)事件的對(duì)象都是UIResponder的子類對(duì)象,例如UIView、UIViewControllerUIApplication等)。當(dāng)事件來到時(shí),系統(tǒng)會(huì)將事件傳遞給合適的響應(yīng)者,并且將其成為第一響應(yīng)者。
    第一響應(yīng)者未處理的事件,將會(huì)在響應(yīng)者鏈中進(jìn)行傳遞,傳遞規(guī)則由UIRespondernextResponder決定,可以通過重寫該屬性來決定傳遞規(guī)則。當(dāng)一個(gè)事件到來時(shí),第一響應(yīng)者沒有接收消息,則順著響應(yīng)者鏈向后傳遞。

如果命中測(cè)試視圖不能處理這個(gè)事件,就會(huì)往上傳遞
五、注意點(diǎn)

在遍歷視圖時(shí),忽略以下三種情況的視圖,如果視圖具有以下特征則忽略:

  1. 視圖的hidden等于YES
  2. 視圖的alpha小于等于0.01。
  3. 視圖的userInteractionEnabledNO

但是視圖的背景顏色是clearColor,并不在忽略范圍內(nèi)。

六、優(yōu)先級(jí)

事件到來后先會(huì)執(zhí)行hitTestpointInside操作,通過這兩個(gè)方法找到第一響應(yīng)者。當(dāng)找到第一響應(yīng)者并將其返回給UIApplication后,UIApplication會(huì)向第一響應(yīng)者派發(fā)事件,并且遍歷整個(gè)響應(yīng)者鏈。開始會(huì)執(zhí)行響應(yīng)者鏈中的touches系列方法。會(huì)先執(zhí)行touchesBegantouchesMoved方法,如果響應(yīng)者鏈能夠繼續(xù)響應(yīng)事件,則執(zhí)行touchesEnded方法表示事件完成。如果響應(yīng)者鏈中有能夠處理當(dāng)前事件的手勢(shì),則將事件交給手勢(shì)處理,調(diào)用touchesCancelled方法將響應(yīng)者鏈打斷。
如果UIButton(所有繼承自UIControl類)是第一響應(yīng)者,則直接由UIApplication派發(fā)事件,不通過響應(yīng)者鏈派發(fā)。如果其不能處理事件,則交給手勢(shì)處理或響應(yīng)者鏈傳遞。

  • 代碼驗(yàn)證
  1. 自定義TestView重寫touches系列方法:
@implementation TestView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan TextView");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved TextView");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesEnded TextView");
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesCancelled TextView");
}
@end
//點(diǎn)擊結(jié)果
touchesBegan TextView
touchesEnded TextView

TestView或者其父控件添加UITapGestureRecognizer點(diǎn)擊手勢(shì)后:

//點(diǎn)擊結(jié)果
touchesBegan TextView
tap
touchesCancelled TextView

view添加單擊手勢(shì)之后,原來的touchesEnded方法就無效了,繼而執(zhí)行touchesCancelled。

  1. 自定義TestButton重寫Tracking系列方法,并添加點(diǎn)擊方法:
@implementation TestButton

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"beginTracking");
    return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"continueTracking");
    return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"endTracking");
}

- (void)cancelTrackingWithEvent:(UIEvent *)event {
    NSLog(@"cancelTracking");
}
//點(diǎn)擊效果
beginTracking
endTracking
buttonToClick

給其父控件添加UITapGestureRecognizer點(diǎn)擊手勢(shì)后:

//點(diǎn)擊效果
beginTracking
endTracking
buttonToClick
  • 優(yōu)先級(jí):系統(tǒng)的UIControl > 手勢(shì) > 自定義的UIControl

如果給TestButton添加UITapGestureRecognizer點(diǎn)擊手勢(shì)后:

//點(diǎn)擊效果
beginTracking
tap
cancelTracking
  • 補(bǔ)充:最后響應(yīng)的途徑便是sendAction分發(fā)event到一個(gè)對(duì)象去處理:
按鈕
手勢(shì)
七、補(bǔ)充點(diǎn)
傳遞與響應(yīng)
  • source1runloop用來處理mach port傳來的系統(tǒng)事件的,source0是用來處理用戶事件的。在之前Runloop執(zhí)行流程中提到過source1source0

推薦參考:https://mp.weixin.qq.com/s/kkWWCb1Zy4d-lPRdPUoVHg
(文章部分來自此參考)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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