iOS 響應者鏈

一、概述

iOS 響應者鏈(Responder Chain)是支撐 App 界面交互的重要基礎,點擊、滑動、旋轉、搖晃等都離不開其背后的響應者鏈鏈。

簡單的說(雖然不準確),響應者鏈的作用就是讓 APP 知道用戶點擊里了哪里,然后應該哪個控件做出反應。專業點說,響應者鏈就是由多個響應者組合起來的鏈條,就叫做響應者鏈。它表示了每個響應者之間的聯系,并且可以使得一個事件可選擇多個對象處理。

二、事件的產生和傳遞

當一個觸摸事件產生的時候,程序是如何找到第一響應者的呢?也就是說程序怎么知道點擊了哪個控件呢?

事件的產生與傳遞

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

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

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

通過在顯示視圖層級中依次對視圖調用這個 2 個方法來確認該視圖是不是能響應這個點擊的點,首先會調用 hitTest,然后在 hitTest 中調用 pointInside,最終 hitTest 返回的那個 view 就是最終的響應者Responder

image.png

以上圖為例,假設點擊了 View3

首先調用 UIWindow 的 hitTest:withEvent:,在這個方法中調用 pointInside:withEvent: 方法判斷點擊是否在 UIWindow 的范圍內,顯然 pointInside 返回 YES

然后遍歷 window 的子視圖,來到 RootView,調用 RootView 的 hitTestpointInside,因為點擊發生在 RootView 中所以繼續遍歷它的子視圖。

從 View2 開始的,調用 View2 的 hitTestpointInside,由于觸摸點在 View2 的范圍內,所以 pointInside 返回 YES;然后繼續遍歷 View2 的子視圖,從 View4 開始,因為點擊不發生在 View4, 所以 pointInside 返回 NO,而 View4 沒有子視圖,所以 hitTest 返回 null;然后繼續在 View2 的另外一個子視圖 View3 中調用 hitTestpointInside,因為點擊的就是 View3 所以 pointInside 返回 YES,且 View3 沒有子視圖,所以 hitTest 返回了自己 View3;接著 View2hitTest 也返回 View3,RootView 的 hitTest 也返回 View3,直到 UIWindow 也返回 View3,自此我們找到了響應視圖:View3。

打印過程

小結:

  1. 尋找事件的響應視圖是通過調用視圖的 hitTestpointInside 完成的。
  2. hitTest 的調用順序是從 UIWindow 開始,對視圖的每個子視圖依次調用。
  3. 遍歷直到找到響應視圖,然后逐級返回最終到 UIWindow 返回此視圖,哪怕還有未遍歷到的視圖,也不會去遍歷了。

三、響應者

所有繼承 UIResponder 的控件都有一個 nextResponder 屬性,此屬性會返回在 Responder Chain 中的下一個事件處理者,如果每個 Responder 都不處理事件,那么事件將會被丟棄。所以繼承自 UIResponder 的子類便會構成一條響應者鏈,所以我們可以打印下以 View3 為開始的響應者鏈是什么樣的:

image.png

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

所以下一個響應者的規則是:如果有父視圖,則 nextResponder 指向父視圖,如果是控制器根視圖則指向控制器,控制器如果在導航控制器中,則指向導航控制器的相關顯示視圖,最后指向導航控制器,如果是根控制器則指向 UIWindow,UIWindow 的 nexResponder 指向 UIApplication,最后指向 AppDelegate,而他們實現這一套指向都是靠重寫 nextReponder 實現的。

繼續接著上面的例子,當點擊了 View3,先是由 UIWindow 通過 hitTest 返回所找到響應者 View3;接著會執行 View3 的 touchesBegan,然后是通過 nextResponder 依次是 View2、RootView,完全是按照 nextResponder 鏈條的調用順序,touchesEnded 也是同樣的順序。

touches 執行順序

上面是 View3 不處理點擊事件的情況,接下來我們為 View3 添加一個點擊事件處理,看看又會是什么樣的調用過程:

touches 執行順序

可以看到 touchesBegan 順著 nextResponder 鏈條調用了,但是 View3 處理了事件,去執行了相關是事件處理方法,而 touchesEnded 并沒有得到調用。

小結:

  1. 找到最適合的響應視圖后事件會從此視圖開始沿著響應鏈 nextResponder 傳遞,直到找到處理事件的視圖,如果沒有處理的事件會被丟棄。

  2. 如果視圖有父視圖則 nextResponder 指向父視圖,如果是根視圖則指向控制器,最終指向 AppDelegate, 他們都是通過重寫 nextResponder 來實現。

響應者鏈

以上是視圖可以正常點擊的情況,但在一些情況下,視圖無法點擊,這時候響應者鏈會如何傳遞呢?

無法點擊是的情況總結:

  1. alpha = 0、子視圖的 frame 超出父視圖、userInteractionEnabled = NOhidden = YES 這些情況下,視圖會被忽略,不會調用 hitTest
  2. 若父視圖被忽略,后其所有子視圖也會被忽略。換句話說,若父視圖不可點擊,則其子視圖一定不能點擊。

四、應用示例

1、點擊透傳

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

2、限定點擊區域

給定一個顯示為圓形的視圖,實現只有在點擊區域在圓形里面才視為有效。
我們可以重寫View的pointInside方法來判斷點擊的點是否在圓內,也就是判斷點擊的點到圓心的距離是否小于等于半徑就可以。

@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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 為了方便理解,會分為三步去解說, 1,點擊事件找到對應的點擊的視圖的處理流程,2, 進行具體例子分析. 3, 常用...
    小鄉123閱讀 3,395評論 0 0
  • 一篇搞定事件傳遞、響應者鏈條、hitTest和pointInside的使用發生觸摸事件后,系統會將該事件加入到一個...
    克魯德李閱讀 1,137評論 0 1
  • 一、響應者鏈(Responder Chain) 先來說說響應者對象(Responder Object),顧名思義,...
    像小強一樣活著閱讀 6,888評論 8 76
  • 1、響應鏈的傳遞 Responder一點也不神秘————iOS用戶響應者鏈完全剖析(建議全看)看完上面一篇應該能完...
    RasonWu閱讀 10,416評論 3 36
  • 先來明確幾個概念 響應者對象可以進行事件處理的對象。用戶進行了某個操作,系統會將該操作包裝成一個Event事件對象...
    yaqiong閱讀 196評論 0 0