淺談 iOS 事件的傳遞和響應過程

問題

  • iOSView 的事件究竟是怎麼傳遞響應的?
  • 爲何 父View 關閉了事件響應時,子View 就沒法響應事件? 底層原理?
  • 如何擴大 Button 的點擊範圍 ?
  • 如何讓 父View子View 同時響應同一事件?默認狀況下只會響應 子View 的事件回調。
  • 爲何 子View 關閉了事件,但其 父View 開啓事件的狀況下,點擊 子View 時,父View 能夠正常響應事件?
  • 爲何 子View 是 UIView時,若是沒有添加手勢,點擊子 View時,會由其父View來響應,而 子View 是 UIControl 時,子View 沒有添加手勢,同樣不會由 父View 來響應
  • ...

分析

iOS 的事件能夠分爲三種html

  • Touch Events(觸摸事件)
  • Motion Events(運動事件,好比重力感應和搖一搖等)
  • Remote Events(遠程事件,好比用耳機上得按鍵來控制手機)

下面主要講解 Touch Events(觸摸事件) Touch Events事件的整個過程能夠分爲 傳遞響應 2 個階段,ios

  • 傳遞: 是當咱們觸摸屏幕時,爲咱們找出最適合的 View
  • 響應: 當咱們找出最適合的 View 後,此時只是找到了最合適的 View,但未必 此 View 能夠響應此事件,因此須要繼續找出能響應此事件的 View

傳遞過程

每當手指接觸屏幕,操做系統會把事件傳遞給當前的 App, 在 UIApplication接收到手指的事件以後,就會去調用`UIWindow的hitTest:withEvent:,看看當前點擊的點是否是在window內,若是是則繼續依次調用其 subView的hitTest:withEvent:方法,直到找到最後須要的view。調用結束而且hit-test view肯定以後,即可以肯定最合適的 View。git

  • 引用幾張圖來講明

遞歸是向界面的根節點UIWindow發送hitTest:withEvent:消息開始的,從這個消息返回的是一個UIView,也就是手指當前位置最前面的那個 hittest view。 當向UIWindow發送hitTest:withEvent:消息時,hitTest:withEvent:裏面所作的事,就是判斷當前的點擊位置是否在window裏面,若是在則遍歷window的subview而後依次對subview發送hitTest:withEvent:消息(注意這裏給subview發送消息是根據當前subview的index順序,index越大就越先被訪問)。若是當前的point沒有在view上面,那麼這個view的subview也就不會被遍歷了。當事件遍歷到了view B.1,發現point在view B.1裏面,而且view B.1沒有subview,那麼他就是咱們要找的hittest view了,找到以後就會一路返回直到根節點,而view B以後的view A也不會被遍歷了。github

  • 下面是 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 方法的內部實現
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
        return nil;
    } else {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
            if (hitView) {
                return hitView;
            }
        }
        return self;
    }
}

複製代碼

上面的代碼來自這裏bash

響應過程

  • 我的對響應過程的理解以下:

當咱們知道最合適的 View 後,事件會 由上向下【子view -> 父view,控制器view -> 控制器】來找出合適響應事件的 View,來響應相關的事件。若是當前的 View 有添加手勢,那麼直接響應相應的事件,不會繼續向下尋找了,若是沒有手勢事件,那麼會看其是否實現了以下的方法:app

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
	- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
	- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
	- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
複製代碼

若是有實現那麼就由此 View 響應,若是沒有實現,那麼就會傳遞給他的下一個響應者【子view -> 父view,控制器view -> 控制器】, 這裏咱們能夠作一個簡單的驗證,在默認狀況下 UIView 是不響應事件的,UIControl 就算沒有添加手勢同樣的會由他來響應, 這裏可使用 runtime查看 UIView 和 UIControl 的方法列表, 或 查看 UIKit 源碼 可知, UIView 沒有實現如上的 touchesBegan方法,而 UIControl 是實現瞭如上的相關方法,因此驗證了剛纔的 UIView 不響應,和 UIControl 的響應。一旦找到最合適響應的View就結束, 在執行響應的綁定的事件,若是沒有就拋棄此事件。iview

個人驗證ide

  • 首先處理添加了手勢時,其即可以處理事件。
  • 咱們建立一個view A 在 A 中添加一個 view B, 若是咱們給 A 加了手勢,B沒有加手勢,
  • 咱們在點擊 B 時,會響應 A 的事件,很是正常的狀況,那麼它是怎麼判斷 B 是否能夠處理的呢?
  • 咱們如今給 B 加一個手勢,那麼一樣的操做時會觸發 B 的手勢,如今咱們 給 B 增長一個方法,
@implementation BMSonView
	- (NSArray<UIGestureRecognizer *> *)gestureRecognizers {
	    NSLog(@"%@", self);
	    return @[];
	}
複製代碼

手勢返回 @[],此時點擊 B 只會觸發 A 的事件,由此能夠說明在判斷 view 是否能夠處理事件實現是判斷 gestureRecognizers 便是否添加了手勢,上面提到了還有判斷以下的方法是否實現了,默認狀況下 UIView 是沒有實現以下的方法的,使用在沒有添加手勢時他不響應事件。ui

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
複製代碼

若是咱們手動實現瞭如上的方法時,就算沒有給 B 添加手勢,點擊 B 時, 事件不會響應 A 的方法,會到上面的方法中。從 UIControl 的源碼即可清除看到。spa

因此我的理解:

  • 事件在傳遞時和上面的 hit 方法有關,一層層向上傳遞,【窗口---> view】由其相應的 view 中具體的實現來肯定誰纔是是最合適響應的view

  • 在響應時,又上向下找出第一個能處理的view來處理事件,[view ---> 窗口],在尋找剛過程當中 會判斷是否增長了手勢 和是否實現瞭如上的 觸摸方法。

  • 至於 UIControl Button 的特殊事件相應,我的認爲是在其m文件中實現了上面的4個方法,在這4個方法中作了相關的處理,這裏能夠從 UIControl 代碼中在知道一些內容。

  • 因此若是想本身實現 UIControl Button ,首先要想辦法處理好上面的4個方法。

  • 圖以下

問題解答

  • iOS 中 View 的事件究竟是怎麼傳遞和響應的?

如上所描。

  • 爲何 父View 關閉了事件響應時,子View 就沒法響應事件?

由於在事件傳遞的時,先到父view,當父view沒法響應事,直接就跳過了遍歷其子view,故只要父類關閉了事件,子 view 就已經沒有機會響應事件了。

  • 如何擴大 Button 的點擊範圍?

擴大點擊範圍,無非就是想原本沒有點擊 btn 但想讓 btn 響應事件,那麼能夠在 hitTest 方法中作適當的操做,當知足xxx條件時,強行返回 btn 來達到最佳點擊範圍的效果,相關的實現能夠自行 Google ,有一些較優雅而簡潔的方式。

  • 如何讓 父View 和 子View 同時響應同一事件?

父View 和 子View同時響應同一事件,默認當點擊子view時,若是ziview能夠處理事件,那麼其餘父view 是不會響應的,可是在 父view 傳到 子view 時咱們在 hitTest 方法中是清楚知道的,使用能夠在這裏作相關的操做便實現了子view 和父view 同時響應事件的效果。

  • 爲何子View 關閉了事件,但其 父View 開啓事件的狀況下,點擊 子View 時,父View 能夠響應事件?

子view關閉了事件,事件的傳遞是 父view 到子view,在 父view時,父view能夠響應,那麼會繼續訪問其 子view是否能夠響應,若是此時子view不能夠響應,那麼他會直接返回 父view,因此 子View 關閉了事件 父View 正常執行事件是必然的。

  • 爲何 子View 是 UIView時,若是沒有添加手勢,點擊子 View時,會由其父View來響應,而 子View 是 UIControl 時,子View 沒有添加手勢,同樣不會由 父View 來響應

這個問題能夠見上面的尋找能夠響應的 view 來解決,UIControl 實現瞭如上的 4 大方法,而 UIView 沒有實現。

  • 這裏其實還有許多內容待挖掘,好比:scrollview 的事件響應等。

參考資料

聲明

相關文章
相關標籤/搜索