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

liangdahong發表於2018-08-01

問題

  • iOSView 的事件到底是怎麼傳遞響應的?
  • 為什麼 父View 關閉了事件響應時,子View 就無法響應事件? 底層原理?
  • 如何擴大 Button 的點選範圍 ?
  • 如何讓 父View子View 同時響應同一事件?預設情況下只會響應 子View 的事件回撥。
  • 為什麼 子View 關閉了事件,但其 父View 開啟事件的情況下,點選 子View 時,父View 可以正常響應事件?
  • 為什麼 子View 是 UIView時,如果沒有新增手勢,點選子 View時,會由其父View來響應,而 子View 是 UIControl 時,子View 沒有新增手勢,一樣不會由 父View 來響應
  • ...

分析

iOS 的事件可以分為三種

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

下面主要講解 Touch Events(觸控事件) Touch Events事件的整個過程可以分為 傳遞響應 2 個階段,

  • 傳遞: 是當我們觸控螢幕時,為我們找出最適合的 View
  • 響應: 當我們找出最適合的 View 後,此時只是找到了最合適的 View,但未必 此 View 可以響應此事件,所以需要繼續找出能響應此事件的 View

傳遞過程

每當手指接觸螢幕,作業系統會把事件傳遞給當前的 App, 在 UIApplication接收到手指的事件之後,就會去呼叫`UIWindow的hitTest:withEvent:,看看當前點選的點是不是在window內,如果是則繼續依次呼叫其 subView的hitTest:withEvent:方法,直到找到最後需要的view。呼叫結束並且hit-test view確定之後,便可以確定最合適的 View。

  • 引用幾張圖來說明

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

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

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

遞迴是向介面的根節點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也不會被遍歷了。

  • 下面是 - (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;
    }
}

複製程式碼

上面的程式碼來自這裡

響應過程

  • 個人對響應過程的理解如下:

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

	- (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就結束, 在執行響應的繫結的事件,如果沒有就拋棄此事件。

我的驗證

  • 首先處理新增了手勢時,其便可以處理事件。
  • 我們建立一個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 是沒有實現如下的方法的,使用在沒有新增手勢時他不響應事件。

- (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 的原始碼便可清除看到。

所以個人理解:

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

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

  • 至於 UIControl Button 的特殊事件相應,個人認為是在其m檔案中實現了上面的4個方法,在這4個方法中做了相關的處理,這裡可以從 UIControl 程式碼中在知道一些內容。

  • 所以如果想自己實現 UIControl Button ,首先要想辦法處理好上面的4個方法。

  • 圖如下

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

問題解答

  • 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 的事件響應等。

參考資料

宣告

相關文章