iOS探索:UI視圖之事件傳遞&視圖響應

事件傳遞

事件傳遞的兩個核心方法

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds 複製代碼

第一個方法返回的是一個UIView,是用來尋找最終哪個視圖來響應這個事件 第二個方法是用來判斷某一個點擊的位置是否在視圖範圍內,若是在就返回YESgit

事件傳遞的流程

WX20181205-193658@2x.png

流程描述
  • 咱們點擊屏幕產生觸摸事件,系統將這個事件加入到一個由UIApplication管理的事件隊列中,UIApplication會從消息隊列裏取事件分發下去,首先傳給UIWindowgithub

  • 在UIWindow中就會調用hitTest:withEvent:方法去返回一個最終響應的視圖bash

  • 在hitTest:withEvent:方法中就回去調用pointInside: withEvent:去判斷當前點擊的point是否在UIWindow範圍內,若是是的話,就會去遍歷它的子視圖來查找最終響應的子視圖ide

  • 遍歷的方式是使用倒序的方式來遍歷子視圖,也就是說最後添加的子視圖會最早遍歷,在每個視圖中都回去調用它的hitTest:withEvent:方法,能夠理解爲是一個遞歸調用ui

  • 最終會返回一個響應視圖,若是返回視圖有值,那麼這個視圖就做爲最終響應視圖,結束整個事件傳遞;若是沒有值,那麼就會將UIWindow做爲響應者spa

hitTest:withEvent:

WX20181205-201230@2x.png

流程描述
  • 首先會判斷當前視圖的hiden屬性、是否能夠交互以及透明度是否大於0.01,若是知足條件則進入下一步,不然返回nilcode

  • 調用pointInside: withEvent:方法來判斷這個點是否在當前視圖範圍內,若是知足條件則進入下一步,不然返回nilcdn

  • 而後以倒序的方式遍歷它的子視圖,在每一個子視圖中去調用hitTest:withEvent:方法,若是有一個子視圖返回了一個最終的響應視圖,那麼就將這個視圖返回給調用方;若是所有遍歷完成都沒有找到一個最終的響應視圖,由於點擊位置在當前視圖範圍內,就將當前視圖做爲最終響應視圖返回對象

實例場景

接下來咱們經過一個具體的實例來進一步的理解事件傳遞,例如:在一個方形按鈕中點擊中間的圓形區域有效,而點擊四角無效blog

按鈕圖片.png

核心思想是在pointInside: withEvent:方法中修改對應的區域

代碼以下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
        return nil;
    }

    //判斷當前視圖是否在點擊範圍內
    if ([self pointInside:point withEvent:event]) {
        //遍歷當前對象的子視圖(倒序)
        __block UIView *hit = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //座標轉換
            CGPoint convertPoint = [self convertPoint:point toView:obj];
            //調用子視圖的hitTest方法
            hit = [obj hitTest:convertPoint withEvent:event];
            //若是找到了就中止遍歷
            if (hit) *stop = YES;
        }];

        //返回當前的視圖對象
        return hit?hit:self;
    }else {
        return nil;
    }
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;
    
    CGFloat x2 = self.frame.size.width / 2;
    CGFloat y2 = self.frame.size.height / 2;
    
    //判斷是否在圓形區域內
    double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    
    if (dis <= self.frame.size.width / 2) {
        return YES;
    }
    else{
        return NO;
    }
}
複製代碼

視圖的響應者鏈

首先咱們要知道事件傳遞和響應過程是相反的

若是hitTest:withEvent:找到了第一響應者initial view,可是該響應者沒有處理該事件,那麼事件會沿着響應者鏈向上傳遞:第一響應者 -> 父視圖 -> 視圖控制器,若是傳遞到最頂級視圖還沒處理事件,那麼就傳遞給UIWindow去處理,若window對象也不處理那麼就交給UIApplication處理,若是UIApplication對象還不處理,就丟棄該事件(可是並不會引發崩潰

而且在iOS中,可以響應事件的對象都是UIResponder的子類對象,UIResponder提供了四個用戶點擊的回調方法,分別對應用戶點擊開始、移動、點擊結束以及取消點擊,其中只有在程序強制退出或者來電時,取消點擊事件纔會調用。

系統回調方法

// UIView是UIResponder的子類,能夠覆蓋下列4個方法處理不一樣的觸摸事件
// 一根或者多根手指開始觸摸view,系統會自動調用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移動,系統會自動調用view的下面方法(隨着手指的移動,會持續調用該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指離開view,系統會自動調用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 觸摸結束前,某個系統事件(例如電話呼入)會打斷觸摸過程,系統會自動調用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch對象
複製代碼

響應者鏈流程圖

WX20181205-205455@2x.png

GitHub

Demo

相關文章
相關標籤/搜索