【iOS面试粮食】UI视图—iOS事件的传递机制
iOS的事件分為3大類型
- Touch Events(觸摸事件)
- Motion Events(運動事件,比如重力感應和搖一搖等)
- Remote Events(遠程事件,比如用耳機上得按鍵來控制手機)
在開發中,最常用到的就是Touch Events(觸摸事件),基本貫穿于每個App中,也是本文的豬腳~ 因此文中所說事件均特指觸摸事件。
接下來,記錄、涉及的問題大致包括:
- 事件是怎么找它的媽媽的?(尋找事件的最佳響應者)
- 事件又是如何去到媽媽的身邊的?媽媽又將如何對待它?(事件的響應及在響應鏈中的傳遞)
尋找事件的最佳響應者(Hit-Testing)
當我們觸摸屏幕的某個可響應的功能點后,最終都會由UIView或者繼承UIView的控件來響應
那我們先來看下UIView的兩個方法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system //返回尋找到的最終響應這個事件的視圖 - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds //判斷某一個點擊的位置是否在視圖范圍內 - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;每個UIView對象都有一個 hitTest: withEvent: 方法,這個方法是Hit-Testing過程中最核心的存在,其作用是詢問事件在當前視圖中的響應者,同時又是作為事件傳遞的橋梁。
看看它是什么時候被調用的
- 當手指接觸屏幕,UIApplication接收到手指的觸摸事件之后,就會去調用UIWindow的hitTest: withEvent:方法
- 在hitTest: withEvent:方法中會調用pointInside: withEvent:去判斷當前點擊的point是否屬于UIWindow范圍內,如果是,就會以倒序的方式遍歷它的子視圖,即越后添加的視圖,越先遍歷
- 子視圖也調用自身的hitTest: withEvent:方法,來查找最終響應的視圖
再來看個示例:
視圖層級如下(同一層級的視圖越在下面,表示越后添加):
A ├── B │ └── D └── C├── E└── F現在假設在E視圖所處的屏幕位置觸發一個觸摸,App接收到這個觸摸事件事件后,先將事件傳遞給UIWindow,然后自下而上開始在子視圖中尋找最佳響應者。事件傳遞的順序如下所示:
- UIWindow將事件傳遞給其子視圖A
- A判斷自身能響應該事件,繼續將事件傳遞給C(因為視圖C比視圖B后添加,因此優先傳給C)。
- C判斷自身能響應事件,繼續將事件傳遞給F(同理F比E后添加)。
- F判斷自身不能響應事件,C又將事件傳遞給E。
- E判斷自身能響應事件,同時E已經沒有子視圖,因此最終E就是最佳響應者。
以上,就是尋找最佳響應者的整個過程。
接下來,來看下hitTest: withEvent:方法里,都做些了什么?
我們已經知道事件在響應者之間的傳遞,是視圖通過判斷自身能否響應事件來決定是否繼續向子視圖傳遞,那么判斷響應的條件是什么呢?
視圖響應事件的條件:
- 允許交互: userInteractionEnabled = YES
- 禁止隱藏:hidden = NO
- 透明度:alpha > 0.01
- 觸摸點的位置:通過 pointInside: withEvent:方法判斷觸摸點是否在視圖的坐標范圍內
代碼的表現大概如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {//3種狀態無法響應事件if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {return nil;}//觸摸點若不在當前視圖上則無法響應事件if ([self pointInside:point withEvent:event]) {//從后往前遍歷子視圖數組for (UIView *subView in [self.subviews reverseObjectEnumerator]) {// 坐標系的轉換,把觸摸點在當前視圖上坐標轉換為在子視圖上的坐標CGPoint convertedPoint = [subView convertPoint:point fromView:self];//詢問子視圖層級中的最佳響應視圖UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];if (hitTestView) {//如果子視圖中有更合適的就返回return hitTestView;}}//沒有在子視圖中找到更合適的響應視圖,那么自身就是最合適的return self;}return nil; }說了這么多,那我們可以運用hitTest: withEvent:來搞些什么事情呢
使超出父視圖坐標范圍的子視圖也能響應事件
視圖層級如下:
A ├── B如上圖所示,視圖B有一部分是不在父視圖A的坐標范圍內的,當我們觸摸視圖B的上半部分,是不會響應事件的。當然,我們可以通過重寫視圖A的 hitTest: withEvent:方法來解決這個需求。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {UIView *view = [super hitTest:point withEvent:event];//如果找不到合適的響應者if (view == nil) {//視圖B坐標系的轉換CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {// 滿足條件,返回視圖Bview = self.deleteButton;}}return view; }在視圖A的hitTest: withEvent:方法中判斷觸摸點,是否位于視圖B的視圖范圍內,如果屬于,則返回視圖B。這樣一來,當我們點擊視圖B的任何位置都可以響應事件了。
事件的響應及在響應鏈中的傳遞
經歷Hit-Testing后,UIApplication已經知道事件的最佳響應者是誰了,接下來要做的事情就是:
- 將事件傳遞給最佳響應者響應
- 事件沿著響應鏈傳遞
事件傳遞給最佳響應者
最佳響應者具有最高的事件響應優先級,因此UIApplication會先將事件傳遞給它供其響應。
UIApplication中有個sendEvent:的方法,在UIWindow中同樣也可以發現一個同樣的方法。UIApplication是通過這個方法把事件發送給UIWindow,然后UIWindow通過同樣的接口,把事件發送給最佳響應者。
以尋找事件的最佳響應者一節中點擊視圖E為例,在EView的 touchesBegan:withEvent: 上打個斷點查看調用棧就能看清這一過程:
當事件傳遞給最佳響應者后,響應者響應這個事件,則這個事件到此就結束了,它會被釋放。假設響應者沒有響應這個事件,那么它將何去何從?事件將會沿著響應鏈自上而下傳遞。
注意: 尋找最佳響應者一節中也說到了事件的傳遞,與此處所說的事件的傳遞有本質區別。上面所說的事件傳遞的目的是為了尋找事件的最佳響應者,是自下而上(父視圖到子視圖)的傳遞;而這里的事件傳遞目的是響應者做出對事件的響應,這個過程是自上而下(子視圖到父視圖)的。前者為“尋找”,后者為“響應”。
事件沿著響應鏈傳遞
在UIKit中有一個類:UIResponder,它是所有可以響應事件的類的基類。來看下它的頭文件的幾個屬性和方法
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>#if UIKIT_DEFINE_AS_PROPERTIES @property(nonatomic, readonly, nullable) UIResponder *nextResponder; #else - (nullable UIResponder*)nextResponder; #endif--------------省略部分代碼------------// Generally, all responders which do custom touch handling should override all four of these methods. // Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each // touch it is handling (those touches it received in touchesBegan:withEvent:). // *** You must handle cancelled touches to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);UIApplication,UIViewController和UIView都是繼承自它,都有一個 nextResponder 方法,用于獲取響應鏈中當前對象的下一個響應者,也通過nextResponder來串成響應鏈。
在App中,所有的視圖都是根據樹狀層次結構組織起來的,因此,每個View都有自己的SuperView。當一個View被add到SuperView上的時候,它的nextResponder屬性就會被指向它的SuperView,各個不同響應者的指向如下:
- UIView 若視圖是控制器的根視圖,則其nextResponder為控制器對象;否則,其nextResponder為父視圖。
- UIViewController 若控制器的視圖是window的根視圖,則其nextResponder為窗口對象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。
- UIWindow nextResponder為UIApplication對象。
- UIApplication 若當前應用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。
這樣,整個App就通過nextResponder串成了一條鏈,也就是我們所說的響應鏈,子視圖指向父視圖構成的響應鏈。
看一下官網對于響應鏈的示例展示
若觸摸發生在UITextField上,則事件的傳遞順序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對象。
響應者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent: 方法控制的,該方法的默認實現是將事件沿著默認的響應鏈往下傳遞。
響應者對于接收到的事件有3種操作:
- 不攔截,默認操作 事件會自動沿著默認的響應鏈往下傳遞
- 攔截,不再往下分發事件 重寫 touchesBegan:withEvent: 進行事件處理,不調用父類的 touchesBegan:withEvent:
- 攔截,繼續往下分發事件 重寫 touchesBegan:withEvent: 進行事件處理,同時調用父類的 touchesBegan:withEvent: 將事件往下傳遞
因此,你也可以通過 touchesBegan:withEvent:方法搞點事情~
總結
觸摸事件先通過自下而上(父視圖–>子視圖)的傳遞方式尋找最佳響應者,
然后以自上而下(子視圖–>父視圖)的方式在響應鏈中傳遞。
總結
以上是生活随笔為你收集整理的【iOS面试粮食】UI视图—iOS事件的传递机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python完美测试数据之faker
- 下一篇: flexsim案例分析3