UI事件傳遞&事件響應

響應鏈工作原理

點擊某一控件到其響應相關事件其實是分為兩步:事件的傳遞與事件的響應

  • 事件分發與傳遞:自上而下
  • 事件響應:自下而上

UIView和CALayer

  • UIView 為其提供內容,以及負責處理觸摸等事件,參與響應鏈
  • CALayer 負責顯示的內容contents
tips:使用了單一職責原則
UIView和CALayer.jpeg

layer是由CALayer的contents決定的,backing store其實就是位圖
backgroundColor是對CALayer同名屬性方法的包裝

事件傳遞與視圖響應機制

/**
 * 返回最終響應事件的視圖
 * @param point
 * @param event
 * @by 打碟的DJ
 */
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

/**
 * 用來判斷某一個點擊的位置是否在當前視圖范圍內
 * @param point
 * @param event
 * @by 打碟的DJ
 */
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

事件傳遞的流程

事件傳遞.png
  • 點擊屏幕的某一個視圖(位置)
  • 事件首先傳遞給UIApplication
  • 在由UIApplication傳遞給當前的UIWindow
  • UIWindow中會通過 hitTest: withEvent: 方法來判斷返回響應的視圖
  • 在上訴方法系統實現的內部會調用 pointInside: withEvent: 方法來判斷當前點擊的位置是否在UIWindow范圍內
  • 如果在的話,會在UIWindow中遍歷它的子視圖(subViews),找到響應的視圖(已倒序的方法進行遍歷)
  • 在每個子視圖中都會調用 hitTest: withEvent: 來判斷是否有響應的視圖
  • 如果有返回的響應的視圖則響應點擊的事件,如果沒有,則不發生任何改變

hitTest: withEvent: 系統實現流程圖

hitTest: withEvent: 系統實現流程圖.png
  • 首先在 hitTest: withEvent: 方法中會判斷當前視圖的hidden屬性、是否可交互(userInteractionEnabled)、透明度(alpha)是否大于0.01

  • 如果當前視圖不是隱藏的、可交互并且透明度大于0.01,才會走后續的流程,否則返回nil

  • 如果都通過了,則會調用pointInside: withEvent:方法來判斷點擊的當前位置是否在當前視圖范圍內

  • 如果返回的是YES,則倒序遍歷當前視圖的所有子視圖,遍歷的過程中會調動所有子視圖的hitTest: withEvent: 方法,如果遍歷過程中有返回點擊的視圖,則結束遍歷,當前視圖響應事件,如果沒有返回,則遍歷當前視圖的同級視圖,直到找到對應的響應事件的視圖,如果遍歷結束后未找到響應事件的視圖,則返回nil,不發生任何變化

      /**
       * 用來判斷控件是否接受事件以及找到最合適的view
       * @param point
       * @param event
       * @by 打碟的DJ
       */
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
          //先判斷當前視圖的hidden屬性,是否可交互,透明度是否大于0.01
          if (self.hidden || !self.userInteractionEnabled || self.alpha <= 0.01) {
              return nil;
          }
          //判斷點擊位置是否在當前視圖中,在則倒序遍歷子視圖
          if ([self pointInside:point withEvent:event]) {
              __block UIView *hit = nil;
              [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                  //坐標轉換
                  CGPoint convertedPoint = [obj convertPoint:point fromView:self];
                  //調用子視圖的hitTest:withEvent方法
                  hit = [obj hitTest:convertedPoint withEvent:event];
                  //如果找到了接收事件的對象,則停止遍歷
                  if (hit) {
                      *stop = YES;
                  }
              }];
              return hit;
          }
          return nil;
      }
    

響應鏈

蘋果官方文檔截取的圖

響應鏈 其實是由一個個UIResponder的子類構成的,UIResponder是系統一個負責接受和處理事件的類。

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

以上這幾個響應觸碰的方法其實也是出自于UIResponder類,而UIView都是繼承自UIResponder

事件的響應流程:

1、首先已確定響應視圖
2、判斷響應視圖是否能響應事件,如果能則事件在響應鏈中終止傳遞。如果不能則將事件傳遞給 nextResponder ,也就是通常的 superview 進行事件響應
3、如果事件繼續上報至 UIWindow 并且仍無法響應,則將事件上報給UIAPplication
4、如果事件繼續上報至 UIAPplication 并且仍無法響應,則將事件上報給它的Delegate,但前提是這個 Delegate 不屬于 響應鏈 并且是 UIResponder 的子類
5、如果最終事件依舊未被響應,則會被系統拋棄

note:

也并非所有的nextResponder都是superview,比如UIViewController的根視圖self.view的nextResponder是其所在UIViewController。而如果UIViewController如果是UIWindow的根控制器,那么它的nextResponder就是UIWindow,但如果UIViewController是另外一個 UIViewController present出來的話,那么它的nextResponder就是之前所執行present操作的那個UIViewController

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容