iOS事件,原來如此

精簡(jiǎn)地說:iOS事件分為傳遞和響應(yīng)兩個(gè)部分。

事件傳遞(建立傳遞鏈):

iOS系統(tǒng)檢測(cè)到手指觸摸(Touch)操作時(shí)會(huì)將其打包成一個(gè)UIEvent對(duì)象,并放入當(dāng)前活動(dòng)Application的事件隊(duì)列,單例的UIApplication會(huì)從事件隊(duì)列中取出觸摸事件并傳遞給單例的UIWindow來處理,UIWindow對(duì)象首先會(huì)使用hitTest:withEvent:方法尋找此次Touch操作初始點(diǎn)所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖,這個(gè)過程稱之為hit-test view。
hittest的目的就是找到最終的傳遞鏈。

hitTest:withEvent:流程如下:

  1. 先判斷當(dāng)前視圖hidden=YES,userInteractionEnabled=NO,alpha<0.01等屬性,如果滿足其中之一,返回nil。
  2. 再看當(dāng)前視圖的pointInside:withEvent:方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi);
  3. 若返回NO,則hitTest:withEvent:返回nil;
  4. 若返回YES,則向當(dāng)前視圖的【一級(jí)子視圖(subviews)遞歸發(fā)送】hitTest:withEvent:消息,所有子視圖的遍歷順序是【從subviews數(shù)組的末尾向前遍歷】,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢;
  5. 若第一次有子視圖返回非空對(duì)象,則hitTest:withEvent:方法返回此對(duì)象,處理結(jié)束;如所有子視圖都返回非,則hitTest:withEvent:方法返回自身。

提醒:
hittest返回nil表示該條傳遞鏈已經(jīng)終止,不是正確的傳遞鏈。
hittest返回非nil對(duì)象表示已經(jīng)找到傳遞鏈的葉子節(jié)點(diǎn),即找到正確的傳遞鏈。

hitTest:withEvent:遇到以下會(huì)返回nil。
1.hidden=YES的視圖。
2.userInteractionEnabled=NO的視圖(注意userInteractionEnabled是影響子視圖事件傳遞,但不影響兄弟視圖)
3.alpha<0.01的視圖。
4.顯示區(qū)域超過父視圖bounds區(qū)域的視圖。那么超出區(qū)域不能識(shí)別。當(dāng)然,可以重寫pointInside:withEvent:方法來識(shí)別。

hitTest:底層實(shí)現(xiàn)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判斷自己能否接收觸摸事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判斷觸摸點(diǎn)在不在自己范圍內(nèi)
    if (![self pointInside:point withEvent:event]) return nil;
    // 3.從后往前遍歷自己的子控件,看是否有子控件更適合響應(yīng)此事件
    int count = self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 沒有找到比自己更合適的view
    return self;
}

結(jié)合例子分析:


例子.png

舉例分析:用戶點(diǎn)擊了View D,下面結(jié)合上圖介紹hit-test view的流程:
1、A是UIWindow的根視圖,因此,UIWindwo對(duì)象會(huì)首先對(duì)A進(jìn)行hit-test;
2、顯然用戶點(diǎn)擊的范圍是在A的范圍內(nèi),因此,pointInside:withEvent:返回了YES,這時(shí)會(huì)繼續(xù)檢查A的子視圖;
3、這時(shí)候會(huì)有兩個(gè)分支,B和C:
C在subviews的末尾,先遞歸遍歷C。點(diǎn)擊的范圍在C內(nèi),即C的pointInside:withEvent:返回YES;
4、這時(shí)候有D和E兩個(gè)分支:
點(diǎn)擊的范圍不再E內(nèi),因此E的pointInside:withEvent:返回NO,對(duì)應(yīng)的hitTest:withEvent:返回nil;
點(diǎn)擊的范圍在D內(nèi),即D的pointInside:withEvent:返回YES,由于D沒有子視圖,因此,D的hitTest:withEvent:會(huì)將D返回,再往回回溯,就是C的hitTest:withEvent:返回D--->>A的hitTest:withEvent:返回D。
即A->C->E(返回nil)->D(返回D)->C(返回D)->A(返回D)
至此,本次點(diǎn)擊事件的第一響應(yīng)者就通過響應(yīng)者鏈的事件分發(fā)邏輯成功的找到了。
不難看出,這個(gè)處理流程有點(diǎn)類似二分搜索的思想,這樣能以最快的速度,最精確地定位出能響應(yīng)觸摸事件的UIView。
另外hittest可能會(huì)被調(diào)用2-3次,這里請(qǐng)不要再hittest作業(yè)務(wù)相關(guān)操作,否則會(huì)導(dǎo)致執(zhí)行多次。

事件響應(yīng)(回溯響應(yīng)鏈):

經(jīng)過hittest后,我們已經(jīng)找到了鏈尾【第一響應(yīng)者】,這時(shí)候開始回溯響應(yīng)操作。響應(yīng)鏈的關(guān)系如圖:
請(qǐng)注意:響應(yīng)鏈在傳遞鏈的基礎(chǔ)上增加了UIViewController。

UIResponder.png

NextResponder:
1.當(dāng)一個(gè)view被添加到superView上的時(shí)候,它的nextResponder就會(huì)被指向它的superView;
2.當(dāng)vc被初始化的時(shí)候,self.view(topmost view)的nextResponder會(huì)被指向所在的controller;
(概括前兩者就是:如果當(dāng)前這個(gè)view是控制器的self.view,那么控制器就是上一個(gè)響應(yīng)者 如果當(dāng)前這個(gè)view不是控制器的view,那么父控件就是上一個(gè)響應(yīng)者)
3.vc的nextResponder會(huì)被指向self.view的superView。
4.最頂級(jí)的vc的nextResponder指向UIWindow。
5.UIWindow的nextResponder指向UIApplication

在事件響應(yīng)對(duì)象UIResponder(UIView和UIViewController都是繼承UIResponder)中有對(duì)應(yīng)的方法來分別處理這幾個(gè)階段的事件:
touchesBegan:NSArray<UITouch *>>withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
Touch默認(rèn)流程(以單擊為例)是:從響應(yīng)鏈葉子節(jié)點(diǎn)開始回溯,先是TouchBegan回溯,然后是TouchEnded回溯。

舉例:對(duì)于觸摸事件來說,UIApplication會(huì)首先把事件交給keyWindow,Window會(huì)將事件交給UIGestureRecognizer處理,如果UIGestureRecognizer識(shí)別了傳遞過來的事件,則交給相對(duì)應(yīng)的target去處理,回溯終止,事件不會(huì)再傳遞!當(dāng)然這里也可以重寫touchBegan等方法手動(dòng)讓手勢(shì)繼續(xù)往superview傳從而實(shí)現(xiàn)多級(jí)響應(yīng)。
如果UIGestureRecognizer并沒有識(shí)別傳遞過來的事件(可能是沒有視圖添加手勢(shì),也可能手勢(shì)識(shí)別不成功),事件會(huì)傳遞到視圖樹形結(jié)構(gòu)

touches方法實(shí)際上什么事都沒做,UIView繼承了它進(jìn)行重寫,就是把事件傳遞給nextResponder,相當(dāng)于[self.nextResponder touchesBegan:touches withEvent:event]。所以當(dāng)一個(gè)view沒有重寫touch事件,那么這個(gè)事件就會(huì)一直傳遞下去,直到UIApplication。如果重寫了touch方法,這個(gè)view響應(yīng)了事件之后,事件就被攔截了,它的nextResponder不會(huì)收到這個(gè)事件。這個(gè)時(shí)候如果想事件繼續(xù)傳遞下去,可以調(diào)用[self.nextResponder touchesBegan:touches withEvent:event]

手勢(shì):

通過touches方法監(jiān)聽view觸摸事件,有很明顯的幾個(gè)缺點(diǎn):必須得自定義view、由于是在view內(nèi)部的touches方法中監(jiān)聽觸摸事件,因此默認(rèn)情況下,無法讓其他外界對(duì)象監(jiān)聽view的觸摸事件、不容易區(qū)分用戶的具體手勢(shì)行為。
所以iOS把觸摸事件做了封裝, 對(duì)常用的手勢(shì)進(jìn)行了處理, 封裝了6種常見的手勢(shì)
UITapGestureRecognizer(敲擊)
UILongPressGestureRecognizer(長(zhǎng)按)
UISwipeGestureRecognizer(輕掃)
UIRotationGestureRecognizer(旋轉(zhuǎn))
UIPinchGestureRecognizer(捏合,用于縮放)
UIPanGestureRecognizer(拖拽)

UIControlEvent:

其實(shí)要了解UIControlEvent,必須簡(jiǎn)單說一下UITouch和UIEvent事件,都是和觸摸相關(guān),UIEvent是一系列UITouch的集合,在IOS中負(fù)責(zé)響應(yīng)觸摸事件。

UIControl是UIView的子類,當(dāng)然也是UIResponder的子類。UIControl是諸如UIButton、UISwitch、UITextField等控件的父類,它本身也包含了一些屬性和方法。

UIControl對(duì)象采用了一種新的事件處理機(jī)制,將觸摸事件轉(zhuǎn)換成簡(jiǎn)單操作,其實(shí)就是重寫了UIResponder的方法中(如touchBegan:withEvent)中,即事件不再往上回溯響應(yīng)。這樣方便了事件處理,而不用每次都重寫TouchBegan方法。
UIResponder可以參考這篇文章UIKit: UIResponder
比如我點(diǎn)擊一個(gè)UIButton,即使你未添加UIControlEventTouchUpInside,它的父類touchBegan也不會(huì)被調(diào)用。因?yàn)閁IControl重寫的方法touchBegan:withEvent并未調(diào)用[super touchBegan:withEvent]

UITapGestureRecognzier:

UITapGestureRecognzier其實(shí)就是對(duì)各類復(fù)雜觸摸操作響應(yīng)過程的一個(gè)封裝。
在六種手勢(shì)識(shí)別中,只有一種手勢(shì)是離散手勢(shì),它就是UITapGestureRecognzier。離散手勢(shì)的特點(diǎn)就是一旦識(shí)別就無法取消,而且只會(huì)調(diào)用一次手勢(shì)操作事件(初始化手勢(shì)時(shí)指定的觸發(fā)方法)。換句話說其他五種手勢(shì)是連續(xù)手勢(shì),連續(xù)手勢(shì)的特點(diǎn)就是會(huì)多次調(diào)用手勢(shì)操作事件,而且在連續(xù)手勢(shì)識(shí)別后可以取消手勢(shì)?!鞠聢D是手勢(shì)狀態(tài)圖】

手勢(shì)狀態(tài).png

分別以UITap和UIPan兩種手勢(shì)說明流程:
UITap:TouchBegan回溯->Tap->TouchCancelled回溯。
UIPan:TouchBegan回溯->TouchMoved多次回溯->UIPanBegan-TouchCancelled回溯->UIPanChanged多次->UIPanEnded。

事件總結(jié):
1.父視圖不能接收事件,則子視圖無法接受事件
2.子視圖超出父視圖的部分,不能接收事件
3.同一個(gè)父視圖下,最上面的視圖,首先遭遇事件,如果能夠響應(yīng),就不向下傳遞事件。如果不能接收,事件向下傳遞

總結(jié):

  1. iOS事件流程分為尋找響應(yīng)鏈和響應(yīng)鏈回溯,其中響應(yīng)鏈回溯部分和android類似。
  2. 為了事件響應(yīng)處理的方便,蘋果又推出了UIControlEvent和UITapGestureRecognzier。它們都是針對(duì)響應(yīng)鏈回溯過程。
  3. UITapGestureRecognzier和UIControlEvent不同,UIControlEvent不會(huì)響應(yīng)父類的TouchBegan等操作,而UITapGestureRecognzier會(huì)響應(yīng)。

參考:
iOS開發(fā)系列--觸摸事件、手勢(shì)識(shí)別、搖晃事件、耳機(jī)線控

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

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

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點(diǎn)擊了?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,535評(píng)論 51 604
  • 在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,249評(píng)論 4 26
  • iOS開發(fā)中的事件處理 理論非原創(chuàng),是對(duì)網(wǎng)上資料的整理以及Demo驗(yàn)證 一. UIResponder 1.1 事件...
    喪心病狂樂閱讀 804評(píng)論 0 0
  • 在原文 http://www.cnblogs.com/mcj-coding/p/3569908.html進(jìn)行了補(bǔ)充...
    WHZ鬧哪樣閱讀 2,485評(píng)論 0 4
  • 本文來自:http://ios.jobbole.com/84081/ 前言: 按照時(shí)間順序,事件的生命周期是這樣的...
    HackerOnce閱讀 2,947評(píng)論 1 10

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