iOS事件分發

來源:互聯網
上載者:User

標籤:

  前段時間項目有一個需求,要在點擊閃屏的時候做一些處理,剛接到這個需求覺得很簡單啊,在原有的view上加個button或者手勢識別啥的,後面實現的時候發現還是有點坑。無論我在閃屏上面加button還是手勢都無法響應到touch事件,後來也想了很多種可能,比如是否訊息傳遞到了其他視圖,可最終發現確是我自己把button從父視圖remove的時候把訊息也給remove了,具體原因是閃屏顯示完成的時候我把button也remove了,而同時顯示閃屏的時候項目也做了很多初始化工作,很佔用主線程,導致UIApplication sendEvent被阻塞了,當主線程閑置下來的時候,我又把button從父視圖remove了,從而一直接收不到touch事件。

  主要也是對事件的整個分發過程不是很瞭解,尋找問題無從下手,因此寫下此文對事件的分發做一個詳細說明,同時也可以在此基礎上做一些拓展。

1.TouchEvents分發流程

  APP的很多頁面跳轉,視圖切換都是通過觸摸事件來觸發的,比如按鈕的點擊,view的手勢等等,那我們點擊螢幕,系統是如何判斷我們點擊的是哪個view的?答案就是HitTest,下面就給大家介紹下整個流程。

  a)當我們點擊裝置的螢幕,UIKit就會產生一個事件對象UIEvent,然後會把這個Event分發給當前APP.

  b)當前APP接收到事件後,UIApplication就會去事件隊列中取最新的事件,然後通過sendEvent分發給能夠處理該事件的對象,但是誰能處理該事件,這個就得靠HitTest來確認了。

  c)HitTest會檢測這個點擊的點是不是發生在這個View上,如果是,就會去遍曆這個View的subviews,直到找到最小的能夠處理事件的view,如果找了一遍沒找到能夠處理的view,則返回自身。當確定了Hit-Test View時,如果當前的application沒有忽略觸摸事件 (UIApplication:isIgnoringInteractionEvents),則application就會去分發事件(sendEvent:->keywindow:sendEvent:)。

  下面舉例來說明下,如所示:

  

  步驟:

  1)當我們點擊了圖中的view 6,因為觸摸點在view 1內,所以檢查view 1的subview view 2 和view 3

  2)觸摸點不在view 2內,觸摸點在view 3內,所以檢查view 3的subview view 5和view 6

  3)觸摸點不在view 5內,觸摸點在view 6 內,並且view 6沒有subview,所以view 6是view 1中包含這個點的最小單位,所以view 6變成了這次觸摸事件的hit-TestView

  說明:

  1)預設的hit-testing順序是按照UIView中Subviews的逆順序

  2)如果View的同層級Subview中有重疊的部分,則優先檢查頂部的Subview,如果頂部的Subview返回nil, 再檢查底部的Subview

  3)Hit-Test也是比較聰明的,檢測過程中有這麼一點,就是說如果點擊沒有發生在某View中,那麼該事件就不可能發生在View的Subview中,所以檢測過程中發現該事件不在ViewB內,也直接就不會檢測在不在ViewF內。也就是說,如果你的Subview設定了clipsToBounds=NO,實際顯示地區可能超出了superView的frame,你點擊超出的部分,是不會處理你的事件的,就是這麼任性!

    4)view提供了兩個方法來配合HitTest:

   - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver‘s coordinate system

     - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds

   5)當一個View收到hitTest訊息時,會調用自己的pointInside:withEvent:方法,如果pointInside返回YES,則表明觸摸事件發生在我自己內部,則會遍曆自己的所有Subview去尋找最小單位(沒有任何子view)的UIView,如果當前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情況的時候,hitTest就不會調用自己的pointInside了,直接返回nil,然後系統就回去遍曆兄弟節點,如下代碼所示:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {    if (self.alpha &lt;= 0.01 || !self.userInteractionEnabled || self.hidden) {        return nil;    }    BOOL inside = [self pointInside:point withEvent:event];    UIView *hitView = nil;    if (inside) {        NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];        for (UIView *subview in enumerator) {            hitView = [subview hitTest:point withEvent:event];            if (hitView) {                break;            }        }        if (!hitView) {            hitView = self;        }        return hitView;    } else {        return nil;    }}

  6)hit-Test 是事件分發的第一步,就算你的app忽略了事件,也會發生hit-Test。確定了hit-TestView之後,才會開始進行下一步的事件分發。

2.應用

  我們可以利用hit-Test做一些事情.

  1) 比如我們點擊了ViewA,我們想讓ViewB響應,這個時候,我們只需要重寫View’s hitTest方法,返回ViewB就可以了,雖然可能用不到,但是偶爾還是會用到的。大概代碼如下:

@interface STPView : UIView @end @implementation STPView - (instancetype)initWithFrame:(CGRect)frame {    self = [super initWithFrame:frame];    if (self) {        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];        button.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);        button.tag = 10001;        button.backgroundColor = [UIColor grayColor];        [button setTitle:@"Button1" forState:UIControlStateNormal];        [self addSubview:button];        [button addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];                UIButton *button2 = [UIButton buttonWithType:UIButtonTypeCustom];        button2.frame = CGRectMake(0, CGRectGetHeight(frame) / 2, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);        button2.tag = 10002;        button2.backgroundColor = [UIColor darkGrayColor];        [button2 setTitle:@"Button2" forState:UIControlStateNormal];        [self addSubview:button2];        [button2 addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];    }    return self;} - (void)_buttonActionFired:(UIButton *)button {    NSLog(@"=====Button Titled %@ ActionFired ", [button titleForState:UIControlStateNormal]);} - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {    UIView *hitView = [super hitTest:point withEvent:event];    if (hitView == [self viewWithTag:10001]) {        return [self viewWithTag:10002];    }    return hitView;} @end

   2) 這裡給大家提供一個Category,來自STKit,這個category的目的就是方便的編寫hitTest方法,由於hitTest方法是override,而不是delegate,所以使用預設的實現方式就比較麻煩。Category如下

/** * @abstract hitTestBlock * * @param 其餘參數 參考UIView hitTest:withEvent: * @param returnSuper 是否返回Super的值。如果*returnSuper=YES,則代表會返回 super hitTest:withEvent:, 否則則按照block的傳回值(即使是nil) *  * @discussion 切記,千萬不要在這個block中調用self hitTest:withPoint,否則則會造成遞迴調用。這個方法就是hitTest:withEvent的一個代替。 */typedef UIView * (^STHitTestViewBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);typedef BOOL (^STPointInsideBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper); @interface UIView (STHitTest)/// althought this is strong ,but i deal it with copy@property(nonatomic, strong) STHitTestViewBlock hitTestBlock;@property(nonatomic, strong) STPointInsideBlock pointInsideBlock; @end

 

@implementation UIView (STHitTest) const static NSString *STHitTestViewBlockKey = @"STHitTestViewBlockKey";const static NSString *STPointInsideBlockKey = @"STPointInsideBlockKey"; + (void)load {    method_exchangeImplementations(class_getInstanceMethod(self, @selector(hitTest:withEvent:)),                                   class_getInstanceMethod(self, @selector(st_hitTest:withEvent:)));    method_exchangeImplementations(class_getInstanceMethod(self, @selector(pointInside:withEvent:)),                                   class_getInstanceMethod(self, @selector(st_pointInside:withEvent:)));} - (UIView *)st_hitTest:(CGPoint)point withEvent:(UIEvent *)event {    NSMutableString *spaces = [NSMutableString stringWithCapacity:20];    UIView *superView = self.superview;    while (superView) {        [spaces appendString:@"----"];        superView = superView.superview;    }    NSLog(@"%@%@:[hitTest:withEvent:]", spaces, NSStringFromClass(self.class));    UIView *deliveredView = nil;    // 如果有hitTestBlock的實現,則調用block    if (self.hitTestBlock) {        BOOL returnSuper = NO;        deliveredView = self.hitTestBlock(point, event, &returnSuper);        if (returnSuper) {            deliveredView = [self st_hitTest:point withEvent:event];        }    } else {        deliveredView = [self st_hitTest:point withEvent:event];    }//    NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));    return deliveredView;} - (BOOL)st_pointInside:(CGPoint)point withEvent:(UIEvent *)event {    NSMutableString *spaces = [NSMutableString stringWithCapacity:20];    UIView *superView = self.superview;    while (superView) {        [spaces appendString:@"----"];        superView = superView.superview;    }    NSLog(@"%@%@:[pointInside:withEvent:]", spaces, NSStringFromClass(self.class));    BOOL pointInside = NO;    if (self.pointInsideBlock) {        BOOL returnSuper = NO;        pointInside =  self.pointInsideBlock(point, event, &returnSuper);        if (returnSuper) {            pointInside = [self st_pointInside:point withEvent:event];        }    } else {        pointInside = [self st_pointInside:point withEvent:event];    }    return pointInside;} - (void)setHitTestBlock:(STHitTestViewBlock)hitTestBlock {    objc_setAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey), hitTestBlock, OBJC_ASSOCIATION_COPY);} - (STHitTestViewBlock)hitTestBlock {    return objc_getAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey));} - (void)setPointInsideBlock:(STPointInsideBlock)pointInsideBlock {    objc_setAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey), pointInsideBlock, OBJC_ASSOCIATION_COPY);} - (STPointInsideBlock)pointInsideBlock {    return objc_getAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey));} @end

   代碼很簡單,就是利用iOS的runtime能力,在hitTest執行之前,插入了一個方法。

  3) iOS7原生的內建NavigationController可以實現從最左側拖動PopViewController(大約13pt),不管當前可見的ViewController有沒有其他的滑動手勢或者事件,這是為什嗎?如何?。下面簡單介紹下:

  如果我們觸摸點的座標 point.x < 13, 我們就讓hit-Test 返回NavigationController.view, 把所有的事件入口交給他,否則就返回super,該怎麼處理怎麼處理,這樣就能滿足我們的條件,即使當前的VC上面有ScrollView,但是由於點擊特定地區的時候,ScrollView根本得不到事件,所以系統會專心處理NavigationController的拖拽手勢,而不是ScrollView的事件,當沒有點擊特定地區的時候,NavigationController的手勢不會觸發,系統會專心處理ScrollView的事件,互不影響。

  雖然iOS8新增了UIScreenEdgePanGestureRecognizer 手勢,但是單純的用這個手勢無法解決當前VC上面有ScrollView的問題。

  當我們確定了HitTestView之後,我們的事件分發就正式開始了,如果hitTestView可以直接處理的,就處理,不能處理的,則交給 The Responder Chain/ GestureRecognizer。

   附上一些測試尋找hitTestView過程中列印的日誌,可以觀察一下:

STPWindow:[hitTest:withEvent:]----UIView:[hitTest:withEvent:]--------STPView:[hitTest:withEvent:]--------UICollectionView:[hitTest:withEvent:]------------UIImageView:[hitTest:withEvent:]------------UIImageView:[hitTest:withEvent:]------------STDefaultRefreshControl:[hitTest:withEvent:]------------STPFeedCell:[hitTest:withEvent:]------------STPFeedCell:[hitTest:withEvent:]----------------UIView:[hitTest:withEvent:]--------------------UIImageView:[hitTest:withEvent:]------------------------UIImageView:[hitTest:withEvent:]------------------------UIView:[hitTest:withEvent:]------------------------STImageView:[hitTest:withEvent:]

 其中—-表示View的階層

 

參考:http://suenblog.duapp.com/blog/100031/iOS事件分發機制(一)%20hit-Testing

 

iOS事件分發

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.