事件的傳遞和響應(yīng)者鏈

當(dāng)我們手指點(diǎn)擊了屏幕上的某一點(diǎn)的時(shí)候,究竟會發(fā)生什么,比如點(diǎn)擊了UIView或者UIButton。這里面就牽扯到事件的分發(fā)傳遞和響應(yīng)鏈。我們先看看事件的分發(fā)吧。

事件的分發(fā)和傳遞。

1.當(dāng)iOS程序中發(fā)生觸摸事件后,系統(tǒng)會將事件加入到UIApplication管理的一個(gè)任務(wù)隊(duì)列中
2.UIApplication將處于任務(wù)隊(duì)列最前端的事件向下分發(fā)。即UIWindow。
3.UIWindow將事件向下分發(fā),即UIView。
4.UIView首先看自己是否能處理事件,觸摸點(diǎn)是否在自己身上。如果能,那么繼續(xù)尋找子視圖。
5.遍歷子控件,重復(fù)以上兩步。
6.如果沒有找到,那么自己就是事件處理者。
7.如果自己不能處理,那么不做任何處理。
其中 UIView不接受事件處理的情況主要有以下三種
1.)view.alpha <0.01
2.)view.userInteractionEnabled = NO
3.)view.hidden = YES
4.)view 超出 superview 的 bounds
這是個(gè)從父控件到子控件尋找處理事件最合適的view的過程,如果父視圖不接受事件處理(上面三種情況),則子視圖也不能接收事件。事件只要觸摸了就會產(chǎn)生,關(guān)鍵在于是否有最合適的view來處理和接收事件,如果遍歷到最后都沒有最合適的view來接收事件,則該事件被廢棄。

怎么尋找最合適的View
//  此方法返回的View是本次點(diǎn)擊事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判斷一個(gè)點(diǎn)是否落在范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

hitTest: withEvent: 是UIView 里面的一個(gè)方法,該方法的作用 在于 : 在視圖的層次結(jié)構(gòu)中尋找一個(gè)最適合的 view 來響應(yīng)觸摸事件。
該方法會被系統(tǒng)調(diào)用,調(diào)用的時(shí)候,如果返回為nil,即事件有可能被丟棄,否則返回最合適的view 來響應(yīng)事件
hitTest 的調(diào)用順序
touch -> UIApplication -> UIWindow -> UIViewController.view -> subViews -> ....-> 合適的view棄

hitTest的實(shí)現(xiàn)思路

根據(jù)上面邏輯以及view不響應(yīng)事件的情況,大概模擬下 hitTest 方法的大概實(shí)現(xiàn)

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{

    // 如果交互未打開,或者透明度小于0.05 或者 視圖被隱藏
    if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
    {

        return nil;
    }

    // 如果 touch 的point 在 self 的bounds 內(nèi)
    if ([self pointInside:point withEvent:event])
    {

        for (UIView *subView in self.subviews)
        {

            //進(jìn)行坐標(biāo)轉(zhuǎn)化
            CGPoint coverPoint = [subView convertPoint:point fromView:self];

           // 調(diào)用子視圖的 hitTest 重復(fù)上面的步驟。找到了,返回hitTest view ,沒找到返回有自身處理
            UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];

            if (hitTestView)
            {

                return hitTestView;
            }
        }

        return self;


    }

    return nil;

}

步驟文字說明

1、首先在當(dāng)前視圖的hitTest方法中調(diào)用pointInside方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi)

2、若pointInside方法返回NO,說明觸摸點(diǎn)不在當(dāng)前視圖內(nèi),則當(dāng)前視圖的hitTest返回nil,該視圖不處理該事件

3、若pointInside方法返回YES,說明觸摸點(diǎn)在當(dāng)前視圖內(nèi),則從最上層的子視圖開始(即從subviews數(shù)組的末尾向前遍歷),遍歷當(dāng)前視圖的所有子視圖,調(diào)用子視圖的hitTest方法重復(fù)步驟1-3

4、直到有子視圖的hitTest方法返回非空對象或者全部子視圖遍歷完畢

5、若第一次有子視圖的hitTest方法返回非空對象,則當(dāng)前視圖的hitTest方法就返回此對象,處理結(jié)束

6、若所有子視圖的hitTest方法都返回nil,則當(dāng)前視圖的hitTest方法返回當(dāng)前視圖本身,最終由該對象處理觸摸事件

結(jié)合下面的例子理解上面的步驟 :


image

其中 : 紅點(diǎn)為touch 點(diǎn)

當(dāng)點(diǎn)擊ViewE時(shí),hitTest執(zhí)行順序如下:
先看看點(diǎn)擊大致走向圖如下,其中,?部分為執(zhí)行pointInside為YES部分,X部分執(zhí)行pointInside為NO部分,最終hitTest返回ViewE。

image

1、首先調(diào)用ViewA的hitTest方法,由于觸摸點(diǎn)在其范圍內(nèi),pointInside返回YES,遍歷其子視圖,依次調(diào)用ViewB和ViewC的hitTest方法

2、執(zhí)行ViewB的hitTest方法,由于觸摸點(diǎn)是不在ViewB內(nèi),其pointInside方法返回NO,hitTest返回nil

3、執(zhí)行ViewC的hitTest方法,由于觸摸點(diǎn)是在ViewC內(nèi),其pointInside方法返回YES,遍歷其子視圖,依次調(diào)用ViewD和ViewE的hitTest方法

4、執(zhí)行ViewD的hitTest方法,由于觸摸點(diǎn)是不在ViewD內(nèi),其pointInside方法返回NO,所以其hitTest返回nil

5、執(zhí)行ViewE的hitTest方法,由于觸摸點(diǎn)是在 ViewE內(nèi),其pointInside方法返回YES,由于其沒有子視圖了,其hitTest返回其本身

6、最終,由ViewE來響應(yīng)該點(diǎn)擊事件

hitTest的運(yùn)用場景

(一)事件穿透


1551276681704.jpg

粉色的遮罩層(maskView) 在黃色 按鈕(btn) 的上層,即 視圖的添加順序?yàn)?,先添?黃色 按鈕(btn),再添加 粉色的遮罩(maskView)。 有這么個(gè)需求,點(diǎn)擊按鈕修改maskView 的背景顏色。

注意 : btn 和 maskview 有重疊部分。視圖都添加到UIViewController.view 。

1>、首先,調(diào)用UIViewController.view的hitTest。

2>、其次,遍歷子視圖,進(jìn)行坐標(biāo)轉(zhuǎn)化,判斷 point 是否在 bounds 內(nèi),發(fā)現(xiàn) maskview 和 btn 都滿足

3>、再者,調(diào)用maskview 和 btn 的hitTest 方法,到這里,我們的目標(biāo)的hitText View 很顯然是btn,那么我們自然就很容易想到,根據(jù) maskview 的isa 找到 類對象,在類對象 重寫 hitTest 方法,當(dāng)hitTestview == self ,返回nil 即可。這樣,事件就別 btn 捕獲到。代碼如下:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{

    UIView *hitTestView = [super hitTest:point withEvent:event];

    if (hitTestView == self) 
    {

        return nil;
    }else
    {

        return hitTestView;
    }

}

(二)子視圖超出父視圖范圍
如圖所示


image

發(fā)布按鈕已然已經(jīng)超出tabbar的范圍,那么該按鈕是如何響應(yīng)點(diǎn)擊事件的?

要讓中間按鈕響應(yīng)點(diǎn)擊超出TabBar按鈕部分的點(diǎn)擊事件,則需要重寫TabBar的hitTest方法了,在執(zhí)行hitTest方法時(shí),判斷點(diǎn)擊區(qū)域在中間按鈕的區(qū)域,則返回中間按鈕,響應(yīng)該事件,代碼如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {


     //將當(dāng)前tabbar的觸摸點(diǎn)轉(zhuǎn)換坐標(biāo)系,轉(zhuǎn)換到中間按鈕的身上,生成一個(gè)新的點(diǎn)
     CGPoint newP = [self convertPoint:point toView:self.centerBtn];

      //判斷如果這個(gè)新的點(diǎn)是在中間按鈕身上,那么處理點(diǎn)擊事件最合適的view就是中間按鈕
      if ( [self.centerBtn pointInside:newP withEvent:event]) 
      {
            return self.centerBtn;
       }


    return [super hitTest:point withEvent:event];

}//重寫hitTest方法,去監(jiān)聽中間按鈕的點(diǎn)擊,目的是為了讓凸出的部分點(diǎn)擊也有反應(yīng)
響應(yīng)者鏈

響應(yīng)鏈?zhǔn)菑淖詈线m的view開始傳遞,處理事件傳遞給下一個(gè)響應(yīng)者,響應(yīng)者鏈的傳遞方法是事件傳遞的反方法,如果所有響應(yīng)者都不處理事件,則事件被丟棄。我們通常用響應(yīng)者鏈來獲取上幾級響應(yīng)者,方法是UIResponder的nextResponder方法。
事件傳遞順序與hitTest 的調(diào)用順序 恰好相反

view -> superView ...- > UIViewController.view -> UIViewController -> UIWindow -> UIApplication -> 事件丟棄

文字說明:

1、 首先由 view 來嘗試處理事件,如果他處理不了,事件將被傳遞到他的父視圖superview

2、superview 也嘗試來處理事件,如果他處理不了,繼續(xù)傳遞他的父視圖
UIViewcontroller.view

3、UIViewController.view嘗試來處理該事件,如果處理不了,將把該事件傳遞給UIViewController

4、UIViewController嘗試處理該事件,如果處理不了,將把該事件傳遞給主窗口Window

5、主窗口Window嘗試來處理該事件,如果處理不了,將傳遞給應(yīng)用單例Application

6、如果Application也處理不了,則該事件將會被丟棄

蘋果官方提供的示意圖如下 :


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容