你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS開發之事件傳遞響應鏈

iOS開發之事件傳遞響應鏈

編輯:IOS開發綜合

當我們在使用微信等工具,點擊掃一掃,就能打開二維碼掃描視圖。在我們點擊屏幕的時候,iphone OS獲取到了用戶進行了“單擊”這一行為,操作系統把包含這些點擊事件的信息包裝成UITouch和UIEvent形式的實例,然後找到當前運行的程序,逐級尋找能夠響應這個事件的對象,直到沒有響應者響應。這一尋找的過程,被稱作事件的響應鏈,如下圖所示,不用的響應者以鏈式的方式尋找

事件響應鏈

一、響應者

在iOS中,能夠響應事件的對象都是UIResponder的子類對象。UIResponder提供了四個用戶點擊的回調方法,分別對應用戶點擊開始、移動、點擊結束以及取消點擊,其中只有在程序強制退出或者來電時,取消點擊事件才會調用。

UIResponder的點擊事件

在自定義UIView為基類的控件時,我們可以重寫這幾個方法來進行點擊回調。在回調中,我們可以看到方法接收兩個參數,一個UITouch對象的集合,還有一個UIEvent對象。這兩個參數分別代表的是點擊對象和事件對象。

1、事件對象
iOS使用UIEvent表示用戶交互的事件對象,在UIEvent.h文件中,我們可以看到有一個UIEventType類型的屬性,這個屬性表示了當前的響應事件類型。分別有多點觸控、搖一搖以及遠程操作(在iOS之後新增了3DTouch事件類型)。在一個用戶點擊事件處理過程中,UIEvent對象是唯一的
2、點擊對象
UITouch表示單個點擊,其類文件中存在枚舉類型UITouchPhase的屬性,用來表示當前點擊的狀態。這些狀態包括點擊開始、移動、停止不動、結束和取消五個狀態。每次點擊發生的時候,點擊對象都放在一個集合中傳入UIResponder的回調方法中,我們通過集合中對象獲取用戶點擊的位置。其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當前點擊坐標點,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個點擊位置的坐標點。
為了確認UIView確實是通過UIResponder的點擊方法響應點擊事件的,我創建了UIView的類別,並重寫+ (void)load方法,使用method_swizzling的方式交換點擊事件的實現

+ (void)load
  Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:));
  Method custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesBegan:withEvent:));
  method_exchangeImplementations(origin, custom);
 
  origin = class_getInstanceMethod([UIView class], @selector(touchesMoved:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesMoved:withEvent:));
  method_exchangeImplementations(origin, custom);
 
  origin = class_getInstanceMethod([UIView class], @selector(touchesEnded:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesEnded:withEvent:));
  method_exchangeImplementations(origin, custom);
}
 
- (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- begin", self.class);
  [self lxd_touchesBegan: touches withEvent: event];
}
 
- (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- move", self.class);
  [self lxd_touchesMoved: touches withEvent: event];
}
 
- (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- end", self.class);
  [self lxd_touchesEnded: touches withEvent: event];
}

在新建的項目中,我分別創建了AView、BView、CView和DView四個UIView的子類,然後點擊任意一個位置:

項目結構圖

在我點擊上圖綠色視圖的時候,控制台輸出了下面的日志(日期部分已經去除):

CView --- begin
CView --- end

由此可見在我們點擊UIView的時候,是通過touches相關的點擊事件進行回調處理的。

除了touches回調的幾個點擊事件,手勢UIGestureRecognizer對象也可以附加在view上,來實現其他豐富的手勢事件。在view添加單擊手勢之後,原來的touchesEnded方法就無效了。最開始我一直認為view添加手勢之後,原有的touches系列方法全部無效。但是在測試demo中,發現view添加手勢之後,touchesBegan方法是有進行回調的,但是moved跟ended就沒有進行回調。因此,在系統的touches事件處理中,在touchesBegan之後,應該是存在著一個調度後續事件(nextHandler)處理的方法,個人猜測事件調度的處理大致如下圖示:

事件調度

二、響應鏈傳遞

上面已經介紹了某個控件在接收到點擊事件時的處理,那麼系統是怎麼通過用戶點擊的位置找到處理點擊事件的view的呢?
在上文我們已經說過了系統通過不斷查找下一個響應者來響應點擊事件,而所有的可交互控件都是UIResponder直接或者間接的子類,那麼我們是否可以在這個類的頭文件中找到關鍵的屬性呢?

正好存在著這麼一個方法:- (nullable UIResponder *)nextResponder,通過方法名我們不難發現這是獲取當前view的下一個響應者,那麼我們重寫touchesBegan方法,逐級獲取下一響應者,直到沒有下一個響應者位置。相關代碼如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  UIResponder * next = [self nextResponder];
  NSMutableString * prefix = @"".mutableCopy;
 
  while (next != nil) {
    NSLog(@"%@%@", prefix, [next class]);
    [prefix appendString: @"--"];
    next = [next nextResponder];
  }  
}

控制台輸出的所有下級事件響應者如下:

AView
--UIView
----ViewController
------UIWindow
--------UIApplication
----------AppDelegate

雖然結果非常有層次,但是從系統逐級查找響應者的角度上來說,這個輸出的順序是剛好相反的。為什麼會出現這種問題呢?我們可以看到輸出中存在一個ViewController類,說明UIViewController也是UIResponder的子類。但是我們可以發現,controller是一個view的管理者,即便它是響應鏈的成員之一,但是按照邏輯來說,控制器不應該是系統查找對象之一,通過nextResponder方法查找的這個思路是不正確的。

後來,發現在UIView的頭文件中存在這麼兩個方法,分別返回UIView和BOOL類型的方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  // default returns YES if point is in bounds

根據方法名,一個是根據點擊坐標返回事件是否發生在本視圖以內,另一個方法是返回響應點擊事件的對象。通過這兩個方法,我們可以猜到,系統在收到點擊事件的時候通過不斷遍歷當前視圖上的子視圖的這些方法,獲取下一個響應的視圖。因此,繼續通過method_swizzling方式修改這兩個方法的實現,並且測試輸出如下:

UIStatusBarWindow can answer 1
UIStatusBar can answer 0
UIStatusBarForegroundView can answer 0
UIStatusBarServiceItemView can answer 0
UIStatusBarDataNetworkItemView can answer 0
UIStatusBarBatteryItemView can answer 0
UIStatusBarTimeItemView can answer 0
hit view: UIStatusBar
hit view: UIStatusBarWindow
UIWindow can answer 1
UIView can answer 1
hit view: _UILayoutGuide
hit view: _UILayoutGuide
AView can answer 1
DView can answer 0
hit view: DView
BView can answer 0
hit view: BView
hit view: AView
hit view: UIView
hit view: UIWindow
...... //下面是touches方法的輸出

最上面的UIStatusBar開頭的類型大家可能沒見過,但是不妨礙我們猜到這是狀態欄相關的一些視圖,具體可以查找蘋果的文檔中心(Xcode中快捷鍵shift+command+0打開)。從輸出中不難看出系統先調用pointInSide: WithEvent:判斷當前視圖以及這些視圖的子視圖是否能接收這次點擊事件,然後在調用hitTest: withEvent:依次獲取處理這個事件的所有視圖對象,在獲取所有的可處理事件對象後,開始調用這些對象的touches回調方法

通過輸出的方法調用,我們可以看到響應查找的順序是: UIStatusBar相關的視圖 -> UIWindow -> UIView -> AView -> DView -> BView(系統在事件鏈傳遞的過程中一定會遍歷所有的子視圖判斷是否能夠響應點擊事件),以本文demo為例,我們可以得出事件響應鏈查找的圖示如下:

響應者查找流程

那麼在上面的查找響應者流程完成之後,系統會將本次事件中的點擊轉換成UITouch對象,然後將這些對象和UIEvent類型的事件對象傳遞給touchesBegan方法,you

不僅如此,從上面輸出的nextResponder來看,所有的響應者都是在查找中返回可響應點擊的視圖。因此,我們可以推測出UIApplication對象維護著自己的一個響應者棧,當pointInSide: withEvent:返回yes的時候,響應者入棧。

響應者棧

棧頂的響應者作為最優先處理事件的對象,假設AView不處理事件,那麼出棧,移交給UIView,以此下去,直到事件得到了處理或者到達AppDelegate後依舊未響應,事件被摒棄為止。通過這個機制我們也可以看到controller是響應者棧中的例外,即便沒有pointInSide: withEvent:的方法返回可響應,controller依舊能夠入棧成為UIView的下一個響應者。

三、響應鏈應用

既然已經知道了系統是怎麼獲取響應視圖的流程了,那麼我們可以通過重寫查找事件處理者的方法來實現不規則形狀點擊。最常見的不規則視圖就是圓形視圖,在demo中我設置view的寬高為200,那麼重寫方法事件如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
  const CGFloat halfWidth = 100;
  CGFloat xOffset = point.x - 100;
  CGFloat yOffset = point.y - 100;
  CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
  return radius <= halfWidth;
}

最終的效果圖如下:

以上就是本文的全部內容,希望對大家的學習有所幫助。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved