1. 程式人生 > >ios事件傳遞和響應

ios事件傳遞和響應

ios事件傳遞分為兩個步驟,一、尋找觸發檢視  二、事件傳遞響應

一、尋找觸發檢視

第一響應者(First responder)指的是當前接受觸控的響應者物件(通常是一個UIView物件),即表示當前該物件正在與使用者互動,它是響應者鏈的開端。整個響應者鏈和事件分發的使命都是找出第一響應者。

UIWindow物件以訊息的形式將事件傳送給第一響應者,使其有機會首先處理事件。如果第一響應者沒有進行處理,系統就將事件(通過訊息)傳遞給響應者鏈中的下一個響應者,看看它是否可以進行處理。

iOS系統檢測到手指觸控(Touch)操作時會將其打包成一個UIEvent物件,並放入當前活動Application的事件佇列,單例的UIApplication會從事件佇列中取出觸控事件並傳遞給單例的UIWindow來處理,UIWindow物件首先會使用hitTest:withEvent:方法尋找此次Touch操作初始點所在的檢視(View),即需要將觸控事件傳遞給其處理的檢視,這個過程稱之為hit-test view。

UIWindow例項物件會首先在它的內容檢視上呼叫hitTest:withEvent:,此方法會在其檢視層級結構中的每個檢視上呼叫pointInside:withEvent:(該方法用來判斷點選事件發生的位置是否處於當前檢視範圍內,以確定使用者是不是點選了當前檢視),如果pointInside:withEvent:返回YES,則繼續逐級呼叫,直到找到touch操作發生的位置,這個檢視也就是要找的hit-test view。
hitTest:withEvent:方法的處理流程如下:
首先呼叫當前檢視的pointInside:withEvent:方法判斷觸控點是否在當前檢視內;
若返回NO,則hitTest:withEvent:返回nil;
若返回YES,則向當前檢視的所有子檢視(subviews)傳送hitTest:withEvent:訊息,所有子檢視的遍歷順序是從最頂層檢視一直到到最底層檢視,即從subviews陣列的末尾向前遍歷,直到有子檢視返回非空物件或者全部子檢視遍歷完畢;
若第一次有子檢視返回非空物件,則hitTest:withEvent:方法返回此物件,處理結束;
如所有子檢視都返回非,則hitTest:withEvent:方法返回自身(self)。

技術分享

圖二

加入使用者點選了View E,下面結合圖二介紹hit-test view的流程:

1、A是UIWindow的根檢視,因此,UIWindwo物件會首相對A進行hit-test;

2、顯然使用者點選的範圍是在A的範圍內,因此,pointInside:withEvent:返回了YES,這時會繼續檢查A的子檢視;

3、這時候會有兩個分支,B和C:

點選的範圍不再B內,因此B分支的pointInside:withEvent:返回NO,對應的hitTest:withEvent:返回nil;

點選的範圍在C內,即C的pointInside:withEvent:返回YES;

4、這時候有D和E兩個分支:

點選的範圍不再D內,因此DpointInside:withEvent:返回NO,對應的hitTest:withEvent:返回nil;

點選的範圍在E內,即E的pointInside:withEvent:返回YES,由於E沒有子檢視(也可以理解成對E的子檢視進行hit-test時返回了nil),因此,E的hitTest:withEvent:會將E返回,再往回回溯,就是C的hitTest:withEvent:返回E--->>A的hitTest:withEvent:返回E。

至此,本次點選事件的第一響應者就通過響應者鏈的事件分發邏輯成功的找到了。

不難看出,這個處理流程有點類似二分搜尋的思想,這樣能以最快的速度,最精確地定位出能響應觸控事件的UIView。

三、說明

1、如果最終hit-test沒有找到第一響應者,或者第一響應者沒有處理該事件,則該事件會沿著響應者鏈向上回溯,如果UIWindow例項和UIApplication例項都不能處理該事件,則該事件會被丟棄;

2、hitTest:withEvent:方法將會忽略隱藏(hidden=YES)的檢視,禁止使用者操作(userInteractionEnabled=YES)的檢視,以及alpha級別小於0.01(alpha<0.01)的檢視。如果一個子檢視的區域超過父檢視的bound區域(父檢視的clipsToBounds 屬性為NO,這樣超過父檢視bound區域的子檢視內容也會顯示),那麼正常情況下對子檢視在父檢視之外區域的觸控操作不會被識別,因為父檢視的pointInside:withEvent:方法會返回NO,這樣就不會繼續向下遍歷子檢視了。當然,也可以重寫pointInside:withEvent:方法來處理這種情況。

3、我們可以重寫hitTest:withEvent:來達到某些特定的目的,下面的連結就是一個有趣的應用舉例,當然實際應用中很少用到這些。

http://blog.csdn.net/error/404.html?from=http%3a%2f%2fblog.csdn.net%2fzhaoguodongios%2farticle%2fdetails%2f44082821

二、事件傳遞,即響應鏈

每一個應用有一個響應者鏈,我們的檢視結構是一個N叉樹(一個檢視可以有多個子檢視,一個子檢視同一時刻只有一個父檢視),而每一個繼承UIResponder的物件都可以在這個N叉樹中扮演一個節點。當葉節點成為最高響應者的時候,從這個葉節點開始往其父節點開始追朔出一條鏈,那麼對於這一個葉節點來講,這一條鏈就是當前的響應者鏈。響應者鏈將系統捕獲到的UIEvent與UITouch從葉節點開始層層向下分發,期間可以選擇停止分發,也可以選擇繼續向下分發。

例子:

我用SingleView模板建立了一個新的工程,它的主Window上只有一個UIViewController,其View之上有一個Button。這個專案中所有UIResponder的子類所構成的N叉樹為這樣的結構:


那麼他看起來並不像N叉樹,但是不代表者不是一顆N叉樹,當我們專案複雜之後,這個View可不可以有多個UIButton節點?所以他就是一棵樹。

實際上我們要把這棵樹寫完整,應該還要算上UIButton的UILabel和UIImageView,因為他們也是UIReponder的子類。這裡先不考慮了。

我們對UIButton來講,他此時若是葉節點,那麼這時我們針對他所在的響應鏈來說,他在他之前的響應者就應該是我們controller的view(樹中的葉節點比父節點永遠更優先被分發事件,但是並不是說他就能在時間上先響應,我們下面講為什麼)。所以我們嘗試在任意地方列印這個Button的nextReponder物件。nextResponder物件是UIReponder類的例項方法,它會返回任意物件在樹中的上一個響應者例項:

NSLog(@"%@",_testButton.nextResponder);

控制檯輸出訊息:

2013-09-21 03:40:25.989 響應鏈 [614:60b] <UIView: 0x16555e10; frame = (0 0; 320 568); autoresize = RM+BM; layer = <CALayer: 0x16555e70>>


我們可以根據這個UIView的尺寸來得知,他就是我們唯一的控制器中的那個UIView。

接下來我們再列印下這個UIView的下一個響應者是誰:

NSLog(@"%@",_testButton.nextResponder.nextResponder);

輸出:

2013-09-21 03:45:03.914 響應鏈 [621:60b] <RSViewController: 0x15da0e30>

依次看,接著加一個nextResponder:

2013-09-21 03:50:49.428 響應鏈 [669:60b] (null)

為什麼這裡ViewController沒有父親呢?

注意這句程式碼我是寫在ViewDidLoad中,而我們知道這個方法的生命週期比較早,所以我們換個地方寫或者延遲一段時間再列印,兩種方法都可以得到結果(由此可以推理出我們響應者樹的構造過程是在ViewDidLoad週期中來完成的,這個函式會將當前例項的構成的響應者子樹合併到我們整個根樹中):

2013-09-21 03:53:47.304 響應鏈 [681:60b] <UIWindow: 0x14e24200; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14e242e0>; layer = <UIWindowLayer: 0x14e244a0>>

再繼續往上追朔:
double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
         NSLog(@"%@",_testButton.nextResponder.nextResponder.nextResponder.nextResponder);
    });

2013-09-21 03:56:22.043 響應鏈 [690:60b] <UIApplication: 0x15659c00>

再加一個:

2013-09-21 03:56:51.186 響應鏈 [696:60b] <RSAppDelegate: 0x16663520>

那麼我們的appDelegate還有沒有父節點?

2013-09-21 03:57:22.588 響應鏈 [706:60b] (null)

沒有了,注意,一個從葉節點開始分發的事件,最多也就只能分發到我們的AppDelegate了!

這個樹形結構在我們的專案中尤為重要,舉個栗子,如果我們想在一個view中重寫UITouchEvent的4個方法,並且不影響他的父檢視也響應這些事件,就要注意你重寫的方式了,比如我們在ViewController中重寫touchBegan如下:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"ViewController接收到觸控事件");
}

在appDelegate的中同樣也寫上這一段:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"appDelegate接收到觸控事件");
}

那麼究竟是誰被觸發呢?

2013-09-21 04:02:49.405 響應鏈 [743:60b] ViewController 接收到觸控事件

這個很好理解,我剛剛也說了,viewController明顯是appDelegate的子節點,他有事件分發的優先權。如果我們想兩個地方都觸發呢?這裡super一下就可以了:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"ViewController接收到觸控事件");
}
輸出:

2013-09-21 04:07:26.206 響應鏈 [749:60b] appDelegate 接收到觸控事件

2013-09-21 04:07:26.208 響應鏈 [749:60b] ViewController 接收到觸控事件

注意看時間戳,appDelegate雖然優先級別不如ViewController,但是他響應的時間上面足足比ViewController早了0.002秒,我這裡試了幾次,都是相差0.002秒。

那麼我們分析一下這裡的響應者鏈是怎樣工作的:

使用者手指觸控到了UIView上,由於我們沒有重寫UIView的UITouchEvent,所以他裡面和super執行的一樣的,將該事件繼續分發到UIViewController;

UIViewController的TouchBegan被我們重寫了,如果我們不super,那麼我們在這裡寫響應程式碼。事件到這裡就不繼續分發了。可想而知,UIViewController祖先節點:UIWindow,UIApplication,AppDelegate都無權被分發此事件。

如果我們super了TouchBegan,那麼此次觸控事件由

ViewController分發給UIWindow,

UIWindow繼而分發給UIApplication,

UIApplication再分發給AppDelegate,

於是我們在ViewController和appDelegate的touchBegan方法中都捕獲到了這次事件。