問題
iOS
中View
的事件到底是怎麼傳遞
和響應
的?- 為什麼
父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。
- 引用幾張圖來說明
- 圖片表示的內容可用下面的話描述來自這裡
遞迴是向介面的根節點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 中 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 的事件響應等。
參考資料
- smnh.me/hit-testing…
- zhoon.github.io/ios/2015/04…
- southpeak.github.io/blog/2015/0…
- developer.apple.com/library/ios…
- developer.apple.com/library/ios…
- developer.apple.com/library/ios…
- developer.apple.com/library/ios…