iOS 響應(yīng)者鏈

一、概述

iOS 響應(yīng)者鏈(Responder Chain)是支撐 App 界面交互的重要基礎(chǔ),點(diǎn)擊、滑動(dòng)、旋轉(zhuǎn)、搖晃等都離不開(kāi)其背后的響應(yīng)者鏈鏈。

簡(jiǎn)單的說(shuō)(雖然不準(zhǔn)確),響應(yīng)者鏈的作用就是讓 APP 知道用戶點(diǎn)擊里了哪里,然后應(yīng)該哪個(gè)控件做出反應(yīng)。專(zhuān)業(yè)點(diǎn)說(shuō),響應(yīng)者鏈就是由多個(gè)響應(yīng)者組合起來(lái)的鏈條,就叫做響應(yīng)者鏈。它表示了每個(gè)響應(yīng)者之間的聯(lián)系,并且可以使得一個(gè)事件可選擇多個(gè)對(duì)象處理。

二、事件的產(chǎn)生和傳遞

當(dāng)一個(gè)觸摸事件產(chǎn)生的時(shí)候,程序是如何找到第一響應(yīng)者的呢?也就是說(shuō)程序怎么知道點(diǎn)擊了哪個(gè)控件呢?

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

當(dāng)點(diǎn)擊了屏幕會(huì)產(chǎn)生一個(gè)觸摸事件,消息循環(huán)(runloop)會(huì)接收到觸摸事件放到消息隊(duì)列里,UIApplication 會(huì)從消息隊(duì)列里取事件分發(fā)下去,接著需要找到去響應(yīng)這個(gè)事件的最佳視圖,也就是 Responder,所以開(kāi)始的第一步應(yīng)該是找到 Responder,那么又是如何找到的呢?那就不得不引出 UIView 的 2 個(gè)方法:

// 返回此次觸摸事件初始點(diǎn)所在的視圖
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 

// 返回視圖是否包含指定的某個(gè)點(diǎn)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 

通過(guò)在顯示視圖層級(jí)中依次對(duì)視圖調(diào)用這個(gè) 2 個(gè)方法來(lái)確認(rèn)該視圖是不是能響應(yīng)這個(gè)點(diǎn)擊的點(diǎn),首先會(huì)調(diào)用 hitTest,然后在 hitTest 中調(diào)用 pointInside,最終 hitTest 返回的那個(gè) view 就是最終的響應(yīng)者Responder

image.png

以上圖為例,假設(shè)點(diǎn)擊了 View3。

首先調(diào)用 UIWindow 的 hitTest:withEvent:,在這個(gè)方法中調(diào)用 pointInside:withEvent: 方法判斷點(diǎn)擊是否在 UIWindow 的范圍內(nèi),顯然 pointInside 返回 YES;

然后遍歷 window 的子視圖,來(lái)到 RootView,調(diào)用 RootView 的 hitTestpointInside,因?yàn)辄c(diǎn)擊發(fā)生在 RootView 中所以繼續(xù)遍歷它的子視圖。

從 View2 開(kāi)始的,調(diào)用 View2 的 hitTestpointInside,由于觸摸點(diǎn)在 View2 的范圍內(nèi),所以 pointInside 返回 YES;然后繼續(xù)遍歷 View2 的子視圖,從 View4 開(kāi)始,因?yàn)辄c(diǎn)擊不發(fā)生在 View4, 所以 pointInside 返回 NO,而 View4 沒(méi)有子視圖,所以 hitTest 返回 null;然后繼續(xù)在 View2 的另外一個(gè)子視圖 View3 中調(diào)用 hitTestpointInside,因?yàn)辄c(diǎn)擊的就是 View3 所以 pointInside 返回 YES,且 View3 沒(méi)有子視圖,所以 hitTest 返回了自己 View3;接著 View2hitTest 也返回 View3,RootView 的 hitTest 也返回 View3,直到 UIWindow 也返回 View3,自此我們找到了響應(yīng)視圖:View3。

打印過(guò)程

小結(jié):

  1. 尋找事件的響應(yīng)視圖是通過(guò)調(diào)用視圖的 hitTestpointInside 完成的。
  2. hitTest 的調(diào)用順序是從 UIWindow 開(kāi)始,對(duì)視圖的每個(gè)子視圖依次調(diào)用。
  3. 遍歷直到找到響應(yīng)視圖,然后逐級(jí)返回最終到 UIWindow 返回此視圖,哪怕還有未遍歷到的視圖,也不會(huì)去遍歷了。

三、響應(yīng)者

所有繼承 UIResponder 的控件都有一個(gè) nextResponder 屬性,此屬性會(huì)返回在 Responder Chain 中的下一個(gè)事件處理者,如果每個(gè) Responder 都不處理事件,那么事件將會(huì)被丟棄。所以繼承自 UIResponder 的子類(lèi)便會(huì)構(gòu)成一條響應(yīng)者鏈,所以我們可以打印下以 View3 為開(kāi)始的響應(yīng)者鏈?zhǔn)鞘裁礃拥模?/p>

image.png

可以看到響應(yīng)者鏈一直延伸到 AppDelegate,View3 的下一個(gè)是 View2,也就是 View3 的父視圖,View2 下一個(gè)是 RootView,也是父視圖,而 RootView 的下一個(gè)則是 Controller。

所以下一個(gè)響應(yīng)者的規(guī)則是:如果有父視圖,則 nextResponder 指向父視圖,如果是控制器根視圖則指向控制器,控制器如果在導(dǎo)航控制器中,則指向?qū)Ш娇刂破鞯南嚓P(guān)顯示視圖,最后指向?qū)Ш娇刂破?,如果是根控制器則指向 UIWindow,UIWindow 的 nexResponder 指向 UIApplication,最后指向 AppDelegate,而他們實(shí)現(xiàn)這一套指向都是靠重寫(xiě) nextReponder 實(shí)現(xiàn)的。

繼續(xù)接著上面的例子,當(dāng)點(diǎn)擊了 View3,先是由 UIWindow 通過(guò) hitTest 返回所找到響應(yīng)者 View3;接著會(huì)執(zhí)行 View3 的 touchesBegan,然后是通過(guò) nextResponder 依次是 View2、RootView,完全是按照 nextResponder 鏈條的調(diào)用順序,touchesEnded 也是同樣的順序。

touches 執(zhí)行順序

上面是 View3 不處理點(diǎn)擊事件的情況,接下來(lái)我們?yōu)?View3 添加一個(gè)點(diǎn)擊事件處理,看看又會(huì)是什么樣的調(diào)用過(guò)程:

touches 執(zhí)行順序

可以看到 touchesBegan 順著 nextResponder 鏈條調(diào)用了,但是 View3 處理了事件,去執(zhí)行了相關(guān)是事件處理方法,而 touchesEnded 并沒(méi)有得到調(diào)用。

小結(jié):

  1. 找到最適合的響應(yīng)視圖后事件會(huì)從此視圖開(kāi)始沿著響應(yīng)鏈 nextResponder 傳遞,直到找到處理事件的視圖,如果沒(méi)有處理的事件會(huì)被丟棄。

  2. 如果視圖有父視圖則 nextResponder 指向父視圖,如果是根視圖則指向控制器,最終指向 AppDelegate, 他們都是通過(guò)重寫(xiě) nextResponder 來(lái)實(shí)現(xiàn)。

響應(yīng)者鏈

以上是視圖可以正常點(diǎn)擊的情況,但在一些情況下,視圖無(wú)法點(diǎn)擊,這時(shí)候響應(yīng)者鏈會(huì)如何傳遞呢?

無(wú)法點(diǎn)擊是的情況總結(jié):

  1. alpha = 0、子視圖的 frame 超出父視圖、userInteractionEnabled = NO、hidden = YES 這些情況下,視圖會(huì)被忽略,不會(huì)調(diào)用 hitTest。
  2. 若父視圖被忽略,后其所有子視圖也會(huì)被忽略。換句話說(shuō),若父視圖不可點(diǎn)擊,則其子視圖一定不能點(diǎn)擊。

四、應(yīng)用示例

1、點(diǎn)擊透?jìng)?/p>

RootView 有 2 個(gè)重疊在一起的子視圖 View1 和 View2,View2 覆蓋在 View1 上面,如何做到點(diǎn)擊 View1 觸發(fā) View2 的處理邏輯?
很簡(jiǎn)單,設(shè)置 View2 的 userInteractionEnabled = NO 即可。

2、限定點(diǎn)擊區(qū)域

給定一個(gè)顯示為圓形的視圖,實(shí)現(xiàn)只有在點(diǎn)擊區(qū)域在圓形里面才視為有效。
我們可以重寫(xiě)View的pointInside方法來(lái)判斷點(diǎn)擊的點(diǎn)是否在圓內(nèi),也就是判斷點(diǎn)擊的點(diǎn)到圓心的距離是否小于等于半徑就可以。

@implementation CircleView
- (void)awakeFromNib {
    [super awakeFromNib];
    self.layer.cornerRadius = self.frame.size.width / 2.0f;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    const CGFloat radius = self.frame.size.width / 2.0f;
    CGFloat xOffset = point.x - radius;
    CGFloat yOffset = point.y - radius;
    CGFloat distance = sqrt(xOffset * xOffset + yOffset * yOffset);
    return distance <= radius;
}
@end
最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 為了方便理解,會(huì)分為三步去解說(shuō), 1,點(diǎn)擊事件找到對(duì)應(yīng)的點(diǎn)擊的視圖的處理流程,2, 進(jìn)行具體例子分析. 3, 常用...
    小鄉(xiāng)123閱讀 3,459評(píng)論 0 0
  • 一篇搞定事件傳遞、響應(yīng)者鏈條、hitTest和pointInside的使用發(fā)生觸摸事件后,系統(tǒng)會(huì)將該事件加入到一個(gè)...
    克魯?shù)吕?/span>閱讀 1,200評(píng)論 0 1
  • 一、響應(yīng)者鏈(Responder Chain) 先來(lái)說(shuō)說(shuō)響應(yīng)者對(duì)象(Responder Object),顧名思義,...
    像小強(qiáng)一樣活著閱讀 6,955評(píng)論 8 76
  • 1、響應(yīng)鏈的傳遞 Responder一點(diǎn)也不神秘————iOS用戶響應(yīng)者鏈完全剖析(建議全看)看完上面一篇應(yīng)該能完...
    RasonWu閱讀 10,516評(píng)論 3 36
  • 先來(lái)明確幾個(gè)概念 響應(yīng)者對(duì)象可以進(jìn)行事件處理的對(duì)象。用戶進(jìn)行了某個(gè)操作,系統(tǒng)會(huì)將該操作包裝成一個(gè)Event事件對(duì)象...
    yaqiong閱讀 236評(píng)論 0 0

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