iOS開發之事件傳遞響應鏈_IOS

來源:互聯網
上載者:User

當我們在使用微信等工具,點擊掃一掃,就能開啟二維碼掃描視圖。在我們點擊螢幕的時候,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 --- beginCView --- 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 1UIStatusBar can answer 0UIStatusBarForegroundView can answer 0UIStatusBarServiceItemView can answer 0UIStatusBarDataNetworkItemView can answer 0UIStatusBarBatteryItemView can answer 0UIStatusBarTimeItemView can answer 0hit view: UIStatusBarhit view: UIStatusBarWindowUIWindow can answer 1UIView can answer 1hit view: _UILayoutGuidehit view: _UILayoutGuideAView can answer 1DView can answer 0hit view: DViewBView can answer 0hit view: BViewhit view: AViewhit view: UIViewhit 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;}

最終的效果圖如下:

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.