iOS觸摸事件詳解

在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下轧叽,但是遇到了這篇文章罗洗,感覺總結(jié)的已經(jīng)很到位肛跌,特此轉(zhuǎn)載艺配。
作者:Lotheve
鏈接:http://www.reibang.com/p/c294d1bd963d
來源:簡(jiǎn)書著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)衍慎,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處转唉。

好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?
困惑于Cell怎么突然不能點(diǎn)擊了稳捆?
糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求赠法?
亦或是已經(jīng)被響應(yīng)鏈、手勢(shì)眷柔、target-action這一系列響應(yīng)觸摸事件的方式折騰到不會(huì)打Hello World期虾?
現(xiàn)在 是時(shí)候帶你上分了~ (強(qiáng)行YY完畢)

本文主要講解iOS觸摸事件的一系列機(jī)制,涉及的問題大致包括:

  • 觸摸事件由觸屏生成后如何傳遞到當(dāng)前應(yīng)用驯嘱?
  • 應(yīng)用接收觸摸事件后如何尋找最佳響應(yīng)者?實(shí)現(xiàn)原理喳坠?
  • 觸摸事件如何沿著響應(yīng)鏈流動(dòng)鞠评?
  • 響應(yīng)鏈、手勢(shì)識(shí)別器壕鹉、UIControl之間對(duì)于觸摸事件的響應(yīng)有著什么樣的瓜葛剃幌?

tips: iOS中的事件除了觸摸事件聋涨,還包括加速計(jì)事件、遠(yuǎn)程控制事件负乡。由于兩者不在本文討論范疇牍白,因此文中所說事件均特指觸摸事件。

事件的生命周期

當(dāng)指尖觸碰屏幕的那一刻抖棘,一個(gè)觸摸事件就在系統(tǒng)中生成了茂腥。經(jīng)過IPC進(jìn)程間通信,事件最終被傳遞到了合適的應(yīng)用切省。在應(yīng)用內(nèi)歷經(jīng)峰回路轉(zhuǎn)的奇幻之旅后最岗,最終被釋放。大致經(jīng)過如下圖:


觸摸事件流動(dòng)過程 圖片來源(http://qingmo.me/2017/03/04/FlowOfUITouch/)

系統(tǒng)響應(yīng)階段

  1. 手指觸碰屏幕朝捆,屏幕感應(yīng)到觸碰后般渡,將事件交由IOKit處理。

  2. IOKit將觸摸事件封裝成一個(gè)IOHIDEvent對(duì)象芙盘,并通過mach port傳遞給SpringBoard進(jìn)程驯用。

mach port 進(jìn)程端口,各進(jìn)程之間通過它進(jìn)行通信儒老。
SpringBoard.app 是一個(gè)系統(tǒng)進(jìn)程蝴乔,可以理解為桌面系統(tǒng),可以統(tǒng)一管理和分發(fā)系統(tǒng)接收到的觸摸事件贷盲。

  1. SpringBoard進(jìn)程因接收到觸摸事件淘这,觸發(fā)了主線程runloop的source1事件源的回調(diào)。

    此時(shí)SpringBoard會(huì)根據(jù)當(dāng)前桌面的狀態(tài)巩剖,判斷應(yīng)該由誰處理此次觸摸事件铝穷。因?yàn)槭录l(fā)生時(shí),你可能正在桌面上翻頁佳魔,也可能正在刷微博曙聂。若是前者(即前臺(tái)無APP運(yùn)行),則觸發(fā)SpringBoard本身主線程runloop的source0事件源的回調(diào)鞠鲜,將事件交由桌面系統(tǒng)去消耗宁脊;若是后者(即有app正在前臺(tái)運(yùn)行),則將觸摸事件通過IPC傳遞給前臺(tái)APP進(jìn)程贤姆,接下來的事情便是APP內(nèi)部對(duì)于觸摸事件的響應(yīng)了榆苞。

APP響應(yīng)階段

1.APP進(jìn)程的mach port接受到SpringBoard進(jìn)程傳遞來的觸摸事件,主線程的runloop被喚醒霞捡,觸發(fā)了source1回調(diào)坐漏。

2.source1回調(diào)又觸發(fā)了一個(gè)source0回調(diào),將接收到的IOHIDEvent對(duì)象封裝成UIEvent對(duì)象,此時(shí)APP將正式開始對(duì)于觸摸事件的響應(yīng)赊琳。

3.source0回調(diào)內(nèi)部將觸摸事件添加到UIApplication對(duì)象的事件隊(duì)列中街夭。事件出隊(duì)后,UIApplication開始一個(gè)尋找最佳響應(yīng)者的過程躏筏,這個(gè)過程又稱hit-testing板丽,細(xì)節(jié)將在[尋找事件的最佳響應(yīng)者]一節(jié)闡述。另外趁尼,此處開始便是與我們平時(shí)開發(fā)相關(guān)的工作了埃碱。

4.尋找到最佳響應(yīng)者后,接下來的事情便是事件在響應(yīng)鏈中的傳遞及響應(yīng)了弱卡,關(guān)于響應(yīng)鏈相關(guān)的內(nèi)容詳見[事件的響應(yīng)及在響應(yīng)鏈中的傳遞]一節(jié)乃正。事實(shí)上,事件除了被響應(yīng)者消耗婶博,還能被手勢(shì)識(shí)別器或是target-action模式捕捉并消耗掉瓮具。其中涉及對(duì)觸摸事件的響應(yīng)優(yōu)先級(jí),詳見[事件的三徒弟UIResponder凡人、UIGestureRecognizer名党、UIControl]一節(jié)。

5.觸摸事件歷經(jīng)坎坷后要么被某個(gè)響應(yīng)對(duì)象捕獲后釋放挠轴,要么致死也沒能找到能夠響應(yīng)的對(duì)象传睹,最終釋放。至此岸晦,這個(gè)觸摸事件的使命就算終結(jié)了欧啤。runloop若沒有其他事件需要處理,也將重歸于眠启上,等待新的事件到來后喚醒邢隧。

現(xiàn)在,你可以回答第一個(gè)問題了冈在。觸摸事件從觸屏產(chǎn)生后倒慧,由IOKit將觸摸事件傳遞給SpringBoard進(jìn)程,再由SpringBoard分發(fā)給當(dāng)前前臺(tái)APP處理包券。

觸摸纫谅、事件、響應(yīng)者

說了那么多溅固,到底什么是觸摸付秕、什么是事件、什么是響應(yīng)者侍郭?先簡(jiǎn)單科普一下盹牧。

UITouch

源起觸摸

  • 一個(gè)手指一次觸摸屏幕俩垃,就對(duì)應(yīng)生成一個(gè)UITouch對(duì)象励幼。多個(gè)手指同時(shí)觸摸汰寓,生成多個(gè)UITouch對(duì)象。
  • 多個(gè)手指先后觸摸苹粟,系統(tǒng)會(huì)根據(jù)觸摸的位置判斷是否更新同一個(gè)UITouch對(duì)象有滑。若兩個(gè)手指一前一后觸摸同一個(gè)位置(即雙擊),那么第一次觸摸時(shí)生成一個(gè)UITouch對(duì)象嵌削,第二次觸摸更新這個(gè)UITouch對(duì)象(UITouch對(duì)象的 tap count 屬性值從1變成2)毛好;若兩個(gè)手指一前一后觸摸的位置不同,將會(huì)生成兩個(gè)UITouch對(duì)象苛秕,兩者之間沒有聯(lián)系肌访。
  • 每個(gè)UITouch對(duì)象記錄了觸摸的一些信息,包括觸摸時(shí)間艇劫、位置吼驶、階段、所處的視圖店煞、窗口等信息蟹演。
// 觸摸的各個(gè)階段狀態(tài) 
//例如當(dāng)手指移動(dòng)時(shí),會(huì)更新phase屬性到UITouchPhaseMoved顷蟀;手指離屏后酒请,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) { 
    UITouchPhaseBegan, // whenever a finger touches the surface. 
    UITouchPhaseMoved, // whenever a finger moves on the surface. 
    UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event. 
    UITouchPhaseEnded, // whenever a finger leaves the surface. 
    UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
  • 手指離開屏幕一段時(shí)間后,確定該UITouch對(duì)象不會(huì)再被更新將被釋放鸣个。

UIEvent

事件的真身

  • 觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng)羞反,一個(gè)觸摸事件對(duì)應(yīng)一個(gè)UIEvent對(duì)象,其中的 type 屬性標(biāo)識(shí)了事件的類型(之前說過事件不只是觸摸事件)囤萤。
  • UIEvent對(duì)象中包含了觸發(fā)該事件的觸摸對(duì)象的集合昼窗,因?yàn)橐粋€(gè)觸摸事件可能是由多個(gè)手指同時(shí)觸摸產(chǎn)生的。觸摸對(duì)象集合通過 allTouches 屬性獲取阁将。

UIResponder

一切為了滿足它的野心

每個(gè)響應(yīng)者都是一個(gè)UIResponder對(duì)象膏秫,即所有派生自UIResponder的對(duì)象,本身都具備響應(yīng)事件的能力做盅。因此以下類的實(shí)例都是響應(yīng)者:

  • UIView
  • UIViewController
  • UIApplication
  • AppDelegate

響應(yīng)者之所以能響應(yīng)事件缤削,因?yàn)槠涮峁┝?個(gè)處理觸摸事件的方法:

//手指觸碰屏幕,觸摸開始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動(dòng)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開屏幕吹榴,觸摸結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前亭敢,某個(gè)系統(tǒng)事件中斷了觸摸,例如電話呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

這幾個(gè)方法在響應(yīng)者對(duì)象接收到事件的時(shí)候調(diào)用图筹,用于做出對(duì)事件的響應(yīng)帅刀。關(guān)于響應(yīng)者何時(shí)接收到事件以及事件如何沿著響應(yīng)鏈傳遞將在下面章節(jié)說明让腹。

尋找事件的最佳響應(yīng)者(Hit-Testing)

第一節(jié)講過APP接收到觸摸事件后,會(huì)被放入當(dāng)前應(yīng)用的一個(gè)事件隊(duì)列中(PS為什么是隊(duì)列而不是棧扣溺?很好理解因?yàn)橛|摸事件必然是先發(fā)生先執(zhí)行骇窍,切合隊(duì)列FIFO的原則)。

每個(gè)事件的理想宿命是被能夠響應(yīng)它的對(duì)象響應(yīng)后釋放锥余,然而響應(yīng)者諸多腹纳,事件一次只有一個(gè),誰都想把事件搶到自己碗里來驱犹,為避免紛爭(zhēng)嘲恍,就得有一個(gè)先后順序,也就是得有一個(gè)響應(yīng)者的優(yōu)先級(jí)雄驹。因此這就存在一個(gè)尋找事件最佳響應(yīng)者(又稱第一響應(yīng)者 first responder)的過程佃牛,目的是找到一個(gè)具備最高優(yōu)先級(jí)響應(yīng)權(quán)的響應(yīng)對(duì)象(the most appropriate responder object),這個(gè)過程叫做Hit-Testing医舆,那個(gè)命中的最佳響應(yīng)者稱為hit-tested view俘侠。

本節(jié)要探討的問題是:
1.應(yīng)用接收到事件后,如何尋找最佳響應(yīng)者彬向?底層如何實(shí)現(xiàn)兼贡?
2.尋找最佳響應(yīng)者過程中事件的攔截。

事件自下而上的傳遞

應(yīng)用接收到事件后先將其置入事件隊(duì)列中以等待處理娃胆。出隊(duì)后遍希,application首先將事件傳遞給當(dāng)前應(yīng)用最后顯示的窗口(UIWindow)詢問其能否響應(yīng)事件。若窗口能響應(yīng)事件里烦,則傳遞給子視圖詢問是否能響應(yīng)凿蒜,子視圖若能響應(yīng)則繼續(xù)詢問子視圖。子視圖詢問的順序是優(yōu)先詢問后添加的子視圖胁黑,即子視圖數(shù)組中靠后的視圖废封。事件傳遞順序如下:

UIApplication ——> UIWindow ——> 子視圖 ——> ... ——> 子視圖

事實(shí)上把UIWindow也看成是視圖即可,這樣整個(gè)傳遞過程就是一個(gè)遞歸詢問子視圖能否響應(yīng)事件過程丧蘸,且后添加的子視圖優(yōu)先級(jí)高(對(duì)于window而言就是后顯示的window優(yōu)先級(jí)高)漂洋。

具體流程如下:

1.UIApplication首先將事件傳遞給窗口對(duì)象(UIWindow),若存在多個(gè)窗口力喷,則優(yōu)先詢問后顯示的窗口刽漂。

2.若窗口不能響應(yīng)事件,則將事件傳遞其他窗口弟孟;若窗口能響應(yīng)事件贝咙,則從后往前詢問窗口的子視圖。

3.重復(fù)步驟2拂募。即視圖若不能響應(yīng)庭猩,則將事件傳遞給上一個(gè)同級(jí)子視圖窟她;若能響應(yīng),則從后往前詢問當(dāng)前視圖的子視圖蔼水。

4.視圖若沒有能響應(yīng)的子視圖了震糖,則自身就是最合適的響應(yīng)者。

示例:

hit-testing 場(chǎng)景

視圖層級(jí)如下(同一層級(jí)的視圖越在下面徙缴,表示越后添加):

A
├── B
│   └── D
└── C
    ├── E
    └── F

現(xiàn)在假設(shè)在E視圖所處的屏幕位置觸發(fā)一個(gè)觸摸试伙,應(yīng)用接收到這個(gè)觸摸事件事件后,先將事件傳遞給UIWindow于样,然后自下而上開始在子視圖中尋找最佳響應(yīng)者。事件傳遞的順序如下所示:


hit-testing 過程

1.UIWindow將事件傳遞給其子視圖A

2.A判斷自身能響應(yīng)該事件潘靖,繼續(xù)將事件傳遞給C(因?yàn)橐晥DC比視圖B后添加穿剖,因此優(yōu)先傳給C)。

3.C判斷自身能響應(yīng)事件卦溢,繼續(xù)將事件傳遞給F(同理F比E后添加)糊余。

4.F判斷自身不能響應(yīng)事件,C又將事件傳遞給E单寂。

5.E判斷自身能響應(yīng)事件贬芥,同時(shí)E已經(jīng)沒有子視圖,因此最終E就是最佳響應(yīng)者宣决。

Hit-Testing的本質(zhì)

上面講了事件在響應(yīng)者之間傳遞的規(guī)則蘸劈,視圖通過判斷自身能否響應(yīng)事件來決定是否繼續(xù)向子視圖傳遞。那么問題來了:視圖如何判斷能否響應(yīng)事件尊沸?以及視圖如何將事件傳遞給子視圖威沫?
首先要知道的是,以下幾種狀態(tài)的視圖無法響應(yīng)事件:

  • 不允許交互:userInteractionEnabled = NO
  • 隱藏:hidden = YES 如果父視圖隱藏洼专,那么子視圖也會(huì)隱藏棒掠,隱藏的視圖無法接收事件
  • 透明度:alpha < 0.01 如果設(shè)置一個(gè)視圖的透明度<0.01,會(huì)直接影響子視圖的透明度屁商。alpha:0.0~0.01為透明烟很。

hitTest:withEvent:
每個(gè)UIView對(duì)象都有一個(gè)hitTest:withEvent:方法,這個(gè)方法是Hit-Testing過程中最核心的存在蜡镶,其作用是詢問事件在當(dāng)前視圖中的響應(yīng)者雾袱,同時(shí)又是作為事件傳遞的橋梁。
hitTest:withEvent:方法返回一個(gè)UIView對(duì)象帽哑,作為當(dāng)前視圖層次中的響應(yīng)者谜酒。默認(rèn)實(shí)現(xiàn)是:

  • 若當(dāng)前視圖無法響應(yīng)事件,則返回nil
  • 若當(dāng)前視圖可以響應(yīng)事件,但無子視圖可以響應(yīng)事件形病,則返回自身作為當(dāng)前視圖層次中的事件響應(yīng)者
  • 若當(dāng)前視圖可以響應(yīng)事件,同時(shí)有子視圖可以響應(yīng)皮仁,則返回子視圖層次中的事件響應(yīng)者

一開始UIApplication將事件通過調(diào)用UIWindow對(duì)象的hitTest:withEvent:傳遞給UIWindow對(duì)象述么,UIWindow的hitTest:withEvent:在執(zhí)行時(shí)若判斷本身能響應(yīng)事件蝌数,則調(diào)用子視圖的hitTest:withEvent:將事件傳遞給子視圖并詢問子視圖上的最佳響應(yīng)者。最終UIWindow返回一個(gè)視圖層次中的響應(yīng)者視圖給UIApplication度秘,這個(gè)視圖就是hit-testing的最佳響應(yīng)者顶伞。

系統(tǒng)對(duì)于視圖能否響應(yīng)事件的判斷邏輯除了之前提到的3種限制狀態(tài),默認(rèn)能響應(yīng)的條件就是觸摸點(diǎn)在當(dāng)前視圖的坐標(biāo)系范圍內(nèi)剑梳。因此唆貌,hitTest:withEvent:的默認(rèn)實(shí)現(xiàn)就可以推測(cè)了,大致如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 
    //3種狀態(tài)無法響應(yīng)事件 
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil; 
    //觸摸點(diǎn)若不在當(dāng)前視圖上則無法響應(yīng)事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //從后往前遍歷子視圖數(shù)組
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--)
    {
       // 獲取子視圖 
       UIView *childView = self.subviews[i];
       // 坐標(biāo)系的轉(zhuǎn)換,把觸摸點(diǎn)在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)   
       CGPoint childP = [self convertPoint:point toView:childView]; 
       //詢問子視圖層級(jí)中的最佳響應(yīng)視圖
       UIView *fitView = [childView hitTest:childP withEvent:event];
       if (fitView)
       { 
            //如果子視圖中有更合適的就返回
            return fitView;
       }
     } 
     //沒有在子視圖中找到更合適的響應(yīng)視圖垢乙,那么自身就是最合適的 
     return self;
}

值得注意的是pointInside:withEvent:這個(gè)方法锨咙,用于判斷觸摸點(diǎn)是否在自身坐標(biāo)范圍內(nèi)。默認(rèn)實(shí)現(xiàn)是若在坐標(biāo)范圍內(nèi)則返回YES追逮,否則返回NO酪刀。

現(xiàn)在我們?cè)谏鲜鍪纠囊晥D層次中的每個(gè)視圖類中添加下面3個(gè)方法來驗(yàn)證一下之前的分析(注意hitTest:withEvent:pointInside:withEvent:方法都要調(diào)用父類的實(shí)現(xiàn),否則不會(huì)按照默認(rèn)的邏輯來執(zhí)行Hit-Testing):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__); 
    return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__);
}

單點(diǎn)觸摸視圖E钮孵,相關(guān)日志打印如下:

-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[FView hitTest:withEvent:]
-[FView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[EView touchesBegan:withEvent:]

可以看到最終是視圖E先對(duì)事件進(jìn)行了響應(yīng)骂倘,同時(shí)事件傳遞過程也和之前的分析一致。事實(shí)上單擊后從 [AView hitTest:withEvent:] 到 [EView pointInside:withEvent:] 的過程會(huì)執(zhí)行兩遍巴席,兩次傳的是同一個(gè)touch历涝,區(qū)別在于touch的狀態(tài)不同,第一次是begin階段情妖,第二次是end階段睬关。也就是說,應(yīng)用對(duì)于事件的傳遞起源于觸摸狀態(tài)的變化毡证。

Hit-Testing過程中的事件攔截(自定義事件流向)

實(shí)際開發(fā)中可能會(huì)遇到一些特殊的交互需求电爹,需要定制視圖對(duì)于事件的響應(yīng)。例如下面Tabbar的這種情況料睛,中間的原型按鈕是底部Tabbar上的控件丐箩,而Tabbar是添加在控制器根視圖中的。默認(rèn)情況下我們點(diǎn)擊圖中紅色方框中按鈕的區(qū)域恤煞,會(huì)發(fā)現(xiàn)按鈕并不會(huì)得到響應(yīng)屎勘。


hit-testing過程中事件攔截場(chǎng)景

分析一下原因其實(shí)很容易就能明白問題所在。忽略不相關(guān)的控件居扒,視圖層次如下:

RootView
└── TableView
└── TabBar 
    └── CircleButton

點(diǎn)擊紅色方框區(qū)域后概漱,生成的觸摸事件首先傳到UIWindow,然后傳到控制器的根視圖即RootView喜喂。RootView經(jīng)判斷可以響應(yīng)觸摸事件瓤摧,而后將事件傳給了子控件TabBar竿裂。問題就出在這里,因?yàn)橛|摸點(diǎn)不在TabBar的坐標(biāo)范圍內(nèi)照弥,因此TabBar無法響應(yīng)該觸摸事件腻异,hitTest:withEvent:直接返回了nil。而后RootView就會(huì)詢問TableView是否能夠響應(yīng)这揣,事實(shí)上是可以的悔常,因此事件最終被TableView消耗。整個(gè)過程给赞,事件根本沒有傳遞到圓形按鈕机打。

有問題就會(huì)有解決策略。經(jīng)過分析塞俱,發(fā)現(xiàn)原因是hit-Testing的過程中姐帚,事件在傳遞到TabBar的時(shí)候沒能繼續(xù)往CircleButton傳,因?yàn)辄c(diǎn)擊區(qū)域坐標(biāo)不在Tabbar的坐標(biāo)范圍內(nèi)障涯,因此Tabbar被識(shí)別成了無法響應(yīng)事件。既然如此膳汪,我們可以修改事件hit-Testing的過程唯蝶,當(dāng)點(diǎn)擊紅色方框區(qū)域時(shí)讓事件流向原型按鈕。

事件傳遞到TabBar時(shí)遗嗽,TabBar的hitTest:withEvent:被調(diào)用粘我,但是pointInside:withEvent:會(huì)返回NO,如此一來hitTest:withEvent:返回了nil痹换。既然如此征字,可以重寫TabBard的pointInside:withEvent:,判斷當(dāng)前觸摸坐標(biāo)是否在子視圖CircleButton的坐標(biāo)范圍內(nèi)娇豫,若在匙姜,則返回YES,反之返回NO冯痢。這樣一來點(diǎn)擊紅色區(qū)域氮昧,事件最終會(huì)傳遞到CircleButton,CircleButton能夠響應(yīng)事件浦楣,最終事件就由CircleButton響應(yīng)了袖肥。同時(shí)點(diǎn)擊紅色方框以外的非TabBar區(qū)域的情況下,因?yàn)門abBar無法響應(yīng)事件振劳,會(huì)按照預(yù)期由TableView響應(yīng)椎组。代碼如下:

//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{ 
      //將觸摸點(diǎn)坐標(biāo)轉(zhuǎn)換到在CircleButton上的坐標(biāo)
      CGPoint pointTemp = [self convertPoint:point toView:_CircleButton]; 
      //若觸摸點(diǎn)在CricleButton上則返回YES 
      if ([_CircleButton pointInside:pointTemp withEvent:event]) { 
          return YES; 
      }
      //否則返回默認(rèn)的操作
      return [super pointInside:point withEvent:event];
}

這樣一來,點(diǎn)擊紅色方框區(qū)域的按鈕就有效了历恐。

現(xiàn)在第二個(gè)問題也可以回答了寸癌。另外項(xiàng)目中如遇到不按常理出牌的事件響應(yīng)需求专筷,相信你也應(yīng)該可以應(yīng)對(duì)了。

事件的響應(yīng)及在響應(yīng)鏈中的傳遞

經(jīng)歷Hit-Testing后灵份,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了仁堪,接下來要做的事情就是:
1.將事件傳遞給最佳響應(yīng)者響應(yīng)
2.事件沿著響應(yīng)鏈傳遞

事件響應(yīng)的前奏

因?yàn)樽罴秧憫?yīng)者具有最高的事件響應(yīng)優(yōu)先級(jí),因此UIApplication會(huì)先將事件傳遞給它供其響應(yīng)填渠。首先弦聂,UIApplication將事件通過sendEvent:傳遞給事件所屬的window,window同樣通過sendEvent:再將事件傳遞給hit-tested view氛什,即最佳響應(yīng)者莺葫。過程如下:

UIApplication ——> UIWindow ——> hit-tested view

以[尋找事件的最佳響應(yīng)者]一節(jié)中點(diǎn)擊視圖E為例,在EView的touchesBegan:withEvent:上斷點(diǎn)查看調(diào)用棧就能看清這一過程:

touchesBegan調(diào)用棧

那么問題又來了枪眉。這個(gè)過程中捺檬,假如應(yīng)用中存在多個(gè)window對(duì)象,UIApplication是怎么知道要把事件傳給哪個(gè)window的贸铜?window又是怎么知道哪個(gè)視圖才是最佳響應(yīng)者的呢堡纬?

其實(shí)簡(jiǎn)單思考一下,這兩個(gè)過程都是傳遞事件的過程蒿秦,涉及的方法都是sendEvent:烤镐,而該方法的參數(shù)(UIEvent對(duì)象)是唯一貫穿整個(gè)經(jīng)過的線索,那么就可以大膽猜測(cè)必然是該觸摸事件對(duì)象上綁定了這些信息棍鳖。事實(shí)上之前在介紹UITouch的時(shí)候就說過touch對(duì)象保存了觸摸所屬的window及view炮叶,而event對(duì)象又綁定了touch對(duì)象,如此一來渡处,是不是就說得通了镜悉。要是不信的話,那就自定義一個(gè)Window類医瘫,重寫sendEvent:方法侣肄,捕捉該方法調(diào)用時(shí)參數(shù)event的狀態(tài),答案就顯而易見了登下。

sendEvent

至于這兩個(gè)屬性是什么時(shí)候綁定到touch對(duì)象上的茫孔,必然是在hit-testing的過程中唄,仔細(xì)想想hit-testing干的不就是這個(gè)事兒?jiǎn)醻

事件的響應(yīng)

前面介紹UIResponder的時(shí)候說過被芳,每個(gè)響應(yīng)者必定都是UIResponder對(duì)象缰贝,通過4個(gè)響應(yīng)觸摸事件的方法來響應(yīng)事件。每個(gè)UIResponder對(duì)象默認(rèn)都已經(jīng)實(shí)現(xiàn)了這4個(gè)方法畔濒,但是默認(rèn)不對(duì)事件做任何處理剩晴,單純只是將事件沿著響應(yīng)鏈傳遞。若要截獲事件進(jìn)行自定義的響應(yīng)操作,就要重寫相關(guān)的方法赞弥。例如毅整,通過重寫touchesMoved: withEvent:方法實(shí)現(xiàn)簡(jiǎn)單的視圖拖動(dòng)。

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

每個(gè)響應(yīng)觸摸事件的方法都會(huì)接收兩個(gè)參數(shù)绽左,分別對(duì)應(yīng)觸摸對(duì)象集合和事件對(duì)象悼嫉。通過監(jiān)聽觸摸對(duì)象中保存的觸摸點(diǎn)位置的變動(dòng),可以時(shí)時(shí)修改視圖的位置拼窥。視圖(UIView)作為響應(yīng)者對(duì)象戏蔑,本身已經(jīng)實(shí)現(xiàn)了touchesMoved: withEvent:方法,因此要?jiǎng)?chuàng)建一個(gè)自定義視圖(繼承自UIView)鲁纠,重寫該方法总棵。

//MovedView
//重寫touchesMoved方法(觸摸滑動(dòng)過程中持續(xù)調(diào)用)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
      //獲取觸摸對(duì)象 
      UITouch *touch = [touches anyObject]; 
      //獲取前一個(gè)觸摸點(diǎn)位置 
      CGPoint prePoint = [touch previousLocationInView:self]; 
      //獲取當(dāng)前觸摸點(diǎn)位置 
      CGPoint curPoint = [touch locationInView:self]; 
      //計(jì)算偏移量
      CGFloat offsetX = curPoint.x - prePoint.x; 
      CGFloat offsetY = curPoint.y - prePoint.y; 
      //相對(duì)之前的位置偏移視圖
      self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

每個(gè)響應(yīng)者都有權(quán)決定是否執(zhí)行對(duì)事件的響應(yīng),只要重寫相關(guān)的觸摸事件方法即可改含。

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

前面一直在提最佳響應(yīng)者情龄,之所以稱之為“最佳”,是因?yàn)槠渚邆漤憫?yīng)事件的最高優(yōu)先權(quán)(響應(yīng)鏈頂端的男人)捍壤。最佳響應(yīng)者首先接收到事件骤视,然后便擁有了對(duì)事件的絕對(duì)控制權(quán):即它可以選擇獨(dú)吞這個(gè)事件,也可以將這個(gè)事件往下傳遞給其他響應(yīng)者鹃觉,這個(gè)由響應(yīng)者構(gòu)成的視圖鏈就稱之為響應(yīng)鏈尚胞。

需要注意的是,上一節(jié)中也說到了事件的傳遞帜慢,與此處所說的事件的傳遞有本質(zhì)區(qū)別。上一節(jié)所說的事件傳遞的目的是為了尋找事件的最佳響應(yīng)者唯卖,是自下而上的傳遞粱玲;而這里的事件傳遞目的是響應(yīng)者做出對(duì)事件的響應(yīng),這個(gè)過程是自上而下的拜轨。前者為“尋找”抽减,后者為“響應(yīng)”。

響應(yīng)者對(duì)于事件的操作方式:
響應(yīng)者對(duì)于事件的攔截以及傳遞都是通過touchesBegan:withEvent:
方法控制的橄碾,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞卵沉。
響應(yīng)者對(duì)于接收到的事件有3種操作:

  • 不攔截,默認(rèn)操作
    事件會(huì)自動(dòng)沿著默認(rèn)的響應(yīng)鏈往下傳遞

  • 攔截法牲,不再往下分發(fā)事件
    重寫touchesBegan:withEvent:進(jìn)行事件處理史汗,不調(diào)用父類的touchesBegan:withEvent:

  • 攔截,繼續(xù)往下分發(fā)事件
    重寫touchesBegan:withEvent:進(jìn)行事件處理拒垃,同時(shí)調(diào)用父類的touchesBegan:withEvent:將事件往下傳遞

響應(yīng)鏈中的事件傳遞規(guī)則:
每一個(gè)響應(yīng)者對(duì)象(UIResponder對(duì)象)都有一個(gè)nextResponder方法停撞,用于獲取響應(yīng)鏈中當(dāng)前對(duì)象的下一個(gè)響應(yīng)者。因此,一旦事件的最佳響應(yīng)者確定了戈毒,這個(gè)事件所處的響應(yīng)鏈就確定了艰猬。

對(duì)于響應(yīng)者對(duì)象,默認(rèn)的nextResponder實(shí)現(xiàn)如下:

  • UIView
    若視圖是控制器的根視圖埋市,則其nextResponder為控制器對(duì)象冠桃;否則,其nextResponder為父視圖道宅。

  • UIViewController
    若控制器的視圖是window的根視圖食听,則其nextResponder為窗口對(duì)象;若控制器是從別的控制器present出來的培己,則其nextResponder為presenting view controller碳蛋。

  • UIWindow
    nextResponder為UIApplication對(duì)象。

  • UIApplication
    若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對(duì)象省咨,且不是UIView肃弟、UIViewController或app本身,則UIApplication的nextResponder為app delegate零蓉。

responderChain

上圖是官網(wǎng)對(duì)于響應(yīng)鏈的示例展示笤受,若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:

  • UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation

圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的敌蜂,則其nextResponder為UIViewController對(duì)象箩兽;若是直接add在UIWindow上的,則其nextResponder為UIWindow對(duì)象章喉。
可以用以下方式打印一個(gè)響應(yīng)鏈中的每一個(gè)響應(yīng)對(duì)象汗贫,在最佳響應(yīng)者的touchBegin:withEvent:方法中調(diào)用即可(別忘了調(diào)用父類的方法)

- (void)printResponderChain
{
      UIResponder *responder = self; 
      printf("%s",[NSStringFromClass([responder class]) UTF8String]); 
      while (responder.nextResponder) {
           responder = responder.nextResponder;
           printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]); 
      }
}

以上一節(jié)原型按鈕的案例為例,重寫CircleButton的touchBegin:withEvent:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
      [self printResponderChain]; 
      [super touchesBegan:touches withEvent:event];
}

點(diǎn)擊原型按鈕的任意區(qū)域秸脱,打印出的完整響應(yīng)鏈如下:
CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate

另外如果有需要落包,完全可以重寫響應(yīng)者的nextResponder方法來自定義響應(yīng)鏈。

現(xiàn)在摊唇,第三個(gè)問題也解決了咐蝇。

事件的三徒弟UIResponder、UIGestureRecognizer巷查、UIControl

iOS中有序,除了UIResponder能夠響應(yīng)事件,手勢(shì)識(shí)別器岛请、UIControl同樣具備對(duì)事件的處理能力旭寿。當(dāng)這幾者同時(shí)存在于某一場(chǎng)景下的時(shí)候,事件又會(huì)有怎樣的歸宿呢髓需?

拋磚引玉

場(chǎng)景界面如圖:


手勢(shì)沖突場(chǎng)景
代碼不能再簡(jiǎn)單:
- (void)viewDidLoad {
      [super viewDidLoad]; 
      //底部是一個(gè)綁定了單擊手勢(shì)的backView     
      UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTapView)]; 
      [_backView addGestureRecognizer:tap];
      //上面是一個(gè)常規(guī)的tableView 
      _tableMain.tableFooterView = [UIView new];
      //還有一個(gè)和tableView同級(jí)的button 
      [_button addTarget:self action:@selector(buttonTap) forControlEvents:UIControlEventTouchUpInside];
}
- (void)actionTapView{ 
      NSLog(@"backview taped");
}
- (void)buttonTap { 
      NSLog(@"button clicked!");
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{       
      NSLog(@"cell selected!");
}

然后我像往常一樣懷揣著吃奶的自信點(diǎn)擊了cell许师。what??點(diǎn)不動(dòng)微渠??點(diǎn)歪了嗎檀蹋?云芦?再點(diǎn)俯逾,還是沒反應(yīng)>艘荨!我試著短按了一小會(huì)兒cell琉历,依舊沒反應(yīng)W蛊摺旗笔!我不死心,長(zhǎng)按了一會(huì)兒蝇恶,didSelectRowAtIndexPath終于調(diào)了,還算給點(diǎn)面子 - -潘懊。然后我又點(diǎn)了下面的button贿衍,沒有任何問題。but what 舌厨?忿薇?

為了搞清楚狀況,我自定義了相關(guān)的控件類揉燃,均重寫了4個(gè)響應(yīng)觸摸事件的方法以打印日志(每個(gè)重寫的觸摸事件方法都調(diào)用了父類的方法以保證事件默認(rèn)傳遞邏輯)筋栋。

觀察各種情況下的日志現(xiàn)象:

現(xiàn)象一 快速點(diǎn)擊cell

backview taped

現(xiàn)象二 短按cell

-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]

現(xiàn)象三 長(zhǎng)按cell

-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!

現(xiàn)象四 點(diǎn)擊button

-[GLButton touchesBegan:withEvent:]
-[GLButton touchesEnded:withEvent:]
button clicked!

如果上面的現(xiàn)象依舊能讓你舒心地抿上一口咖啡,那么恭喜你抢腐,本節(jié)的內(nèi)容已經(jīng)不適合你了。如果覺得一臉懵逼迈倍,那就繼續(xù)往下看吧~

二師兄—手勢(shì)識(shí)別器

關(guān)于手勢(shì)識(shí)別器即UIGestureRecognizer本身的使用不是本文要所討論的內(nèi)容,按下不表宴合。此處要探討的是:手勢(shì)識(shí)別器與UIResponder的聯(lián)系

事實(shí)上迹鹅,手勢(shì)分為離散型手勢(shì)(discrete gestures)和持續(xù)型手勢(shì)(continuous gesture)卦洽。系統(tǒng)提供的離散型手勢(shì)包括點(diǎn)按手勢(shì)(UITapGestureRecognizer)和輕掃手勢(shì)(UISwipeGestureRecognizer
),其余均為持續(xù)型手勢(shì)斜棚。

兩者主要區(qū)別在于狀態(tài)變化過程:

  • 離散型:
    識(shí)別成功:Possible —> Recognized
    識(shí)別失敺У佟:Possible —> Failed

  • 持續(xù)型:
    完整識(shí)別:Possible —> Began —> [Changed] —> Ended
    不完整識(shí)別:Possible —> Began —> [Changed] —> Cancel

離散型手勢(shì)

先拋開上面的場(chǎng)景,看一個(gè)簡(jiǎn)單的demo打肝。


離散型手勢(shì)場(chǎng)景

控制器的視圖上add了一個(gè)View記為YellowView脂新,并綁定了一個(gè)單擊手勢(shì)識(shí)別器。

// LXFViewController
- (void)viewDidLoad { 
      [super viewDidLoad]; 
      UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTap)]; 
      [self.view addGestureRecognizer:tap];
}
- (void)actionTap{
      NSLog(@"View Taped");
}

單擊YellowView粗梭,日志打印如下:

-[YellowView touchesBegan:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]

從日志上看出YellowView最后Cancel了對(duì)觸摸事件的響應(yīng)争便,而正常應(yīng)當(dāng)是觸摸結(jié)束后滞乙,YellowView的touchesEnded:withEvent:的方法被調(diào)用才對(duì)鉴嗤。另外醉锅,期間還執(zhí)行了手勢(shì)識(shí)別器綁定的action 。我從官方文檔找到了這樣的解釋:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.

大致理解是垄琐,Window在將事件傳遞給hit-tested view之前经柴,會(huì)先將事件傳遞給相關(guān)的手勢(shì)識(shí)別器并由手勢(shì)識(shí)別器優(yōu)先識(shí)別坯认。若手勢(shì)識(shí)別器成功識(shí)別了事件氓涣,就會(huì)取消hit-tested view對(duì)事件的響應(yīng)劳吠;若手勢(shì)識(shí)別器沒能識(shí)別事件恩伺,hit-tested view才完全接手事件的響應(yīng)權(quán)晶渠。

一句話概括:手勢(shì)識(shí)別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級(jí)!便瑟!

按照這個(gè)解釋番川,Window在將事件傳遞給hit-tested view即YellowView之前颁督,先傳遞給了控制器根視圖上的手勢(shì)識(shí)別器。手勢(shì)識(shí)別器成功識(shí)別了該事件屿讽,通知Application取消YellowView對(duì)事件的響應(yīng)吠裆。

然而看日志试疙,卻是YellowView的touchesBegan:withEvent:先調(diào)用了,既然手勢(shì)識(shí)別器先響應(yīng)履澳,不應(yīng)該上面的action先執(zhí)行嗎怀跛,這又怎么解釋敌完?事實(shí)上這個(gè)認(rèn)知是錯(cuò)誤的羊初。手勢(shì)識(shí)別器的action的調(diào)用時(shí)機(jī)(即此處的actionTap)并不是手勢(shì)識(shí)別器接收到事件的時(shí)機(jī),而是手勢(shì)識(shí)別器成功識(shí)別事件后的時(shí)機(jī)闽撤,即手勢(shì)識(shí)別器的狀態(tài)變?yōu)?a target="_blank" rel="nofollow">UIGestureRecognizerStateRecognized脯颜。因此從該日志中并不能看出事件是優(yōu)先傳遞給手勢(shì)識(shí)別器的栋操,那該怎么證明Window先將事件傳遞給了手勢(shì)識(shí)別器?

要解決這個(gè)問題舍沙,只要知道手勢(shì)識(shí)別器是如何接收事件的剔宪,然后在接收事件的方法中打印日志對(duì)比調(diào)用時(shí)間先后即可葱绒。說起來你可能不信,手勢(shì)識(shí)別器對(duì)于事件的響應(yīng)也是通過這4個(gè)熟悉的方法來實(shí)現(xiàn)的失球。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

需要注意的是她倘,雖然手勢(shì)識(shí)別器通過這幾個(gè)方法來響應(yīng)事件作箍,但它并不是UIResponder的子類胞得,相關(guān)的方法聲明在UIGestureRecognizerSubclass.h中。

這樣一來跃巡,我們便可以自定義一個(gè)單擊手勢(shì)識(shí)別器的類牧愁,重寫這幾個(gè)方法來監(jiān)聽手勢(shì)識(shí)別器接收事件的時(shí)機(jī)猪半。創(chuàng)建一個(gè)UITapGestureRecognizer的子類偷线,重寫響應(yīng)事件的方法声邦,每個(gè)方法中調(diào)用父類的實(shí)現(xiàn)摆舟,并替換demo中的手勢(shì)識(shí)別器恨诱。另外需要在.m文件中引入import <UIKit/UIGestureRecognizerSubclass.h>,因?yàn)橄嚓P(guān)方法聲明在該頭文件中材失。

// LXFTapGestureRecognizer (繼承自UITapGestureRecognizer)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__); 
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__); 
    [super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__); 
    [super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__); 
    [super touchesCancelled:touches withEvent:event];
}

現(xiàn)在龙巨,再次點(diǎn)擊YellowView熊响,日志如下:

-[LXFTapGestureRecognizer touchesBegan:withEvent:]
-[YellowView touchesBegan:withEvent:]
-[LXFTapGestureRecognizer touchesEnded:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]

很明顯汗茄,確實(shí)是手勢(shì)識(shí)別器先接收到了事件。之后手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)递览,執(zhí)行了action瞳腌,再由Application取消了YellowView對(duì)事件的響應(yīng)嫂侍。

Window怎么知道要把事件傳遞給哪些手勢(shì)識(shí)別器?

之前探討過Application怎么知道要把event傳遞給哪個(gè)Window菲盾,以及Window怎么知道要把event傳遞給哪個(gè)hit-tested view的問題懒鉴,答案是這些信息都保存在event所綁定的touch對(duì)象上碎浇。手勢(shì)識(shí)別器也是一樣的咆畏,event綁定的touch對(duì)象上維護(hù)了一個(gè)手勢(shì)識(shí)別器數(shù)組,里面的手勢(shì)識(shí)別器毫無疑問是在hit-testing的過程中收集的麦牺。打個(gè)斷點(diǎn)看一下touch上綁定的手勢(shì)識(shí)別器數(shù)組:

手勢(shì)識(shí)別器捕捉

Window先將事件傳遞給這些手勢(shì)識(shí)別器鞭缭,再傳給hit-tested view岭辣。一旦有手勢(shì)識(shí)別器成功識(shí)別了手勢(shì),Application就會(huì)取消hit-tested view對(duì)事件的響應(yīng)仑濒。

持續(xù)型手勢(shì)

將上面Demo中視圖綁定的單擊手勢(shì)識(shí)別器用滑動(dòng)手勢(shì)識(shí)別器(UIPanGestureRecognizer)替換墩瞳。

- (void)viewDidLoad { 
      [super viewDidLoad]; 
      UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan)]; 
      [self.view addGestureRecognizer:pan];
}
- (void)actionPan{ 
      NSLog(@"View panned");
}

在YellowView上執(zhí)行一次滑動(dòng):


持續(xù)型手勢(shì)場(chǎng)景

日志打印如下:

-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesCancelled:withEvent:]
View panned
View panned
View panned
...

在一開始滑動(dòng)的過程中喉酌,手勢(shì)識(shí)別器處在識(shí)別手勢(shì)階段泵喘,滑動(dòng)產(chǎn)生的連續(xù)事件既會(huì)傳遞給手勢(shì)識(shí)別器又會(huì)傳遞給YellowView纪铺,因此YellowView的touchesMoved:withEvent:在開始一段時(shí)間內(nèi)會(huì)持續(xù)調(diào)用;當(dāng)手勢(shì)識(shí)別器成功識(shí)別了該滑動(dòng)手勢(shì)時(shí)和蚪,手勢(shì)識(shí)別器的action開始調(diào)用烹棉,同時(shí)通知Application取消YellowView對(duì)事件的響應(yīng)浆洗。之后僅由滑動(dòng)手勢(shì)識(shí)別器接收事件并響應(yīng),YellowView不再接收事件抠刺。

另外速妖,在滑動(dòng)的過程中,若手勢(shì)識(shí)別器未能識(shí)別手勢(shì)备恤,則事件在觸摸滑動(dòng)過程中會(huì)一直傳遞給hit-tested view锦秒,直到觸摸結(jié)束旅择。讀者可自行驗(yàn)證。

手勢(shì)識(shí)別器的3個(gè)屬性

@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

先總結(jié)一下手勢(shì)識(shí)別器與UIResponder對(duì)于事件響應(yīng)的聯(lián)系:

當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時(shí)沉噩,Window都會(huì)傳遞事件尋求響應(yīng)屁擅。

  • Window先將綁定了觸摸對(duì)象的事件傳遞給觸摸對(duì)象上綁定的手勢(shì)識(shí)別器产弹,再發(fā)送給觸摸對(duì)象對(duì)應(yīng)的hit-tested view痰哨。
  • 手勢(shì)識(shí)別器識(shí)別手勢(shì)期間,若觸摸對(duì)象的觸摸狀態(tài)發(fā)生變化早抠,事件都是先發(fā)送給手勢(shì)識(shí)別器再發(fā)送給hit-test view撬讽。
  • 手勢(shì)識(shí)別器若成功識(shí)別了手勢(shì),則通知Application取消hit-tested view對(duì)于事件的響應(yīng)甘苍,并停止向hit-tested view發(fā)送事件烘豌;
  • 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),而此時(shí)觸摸并未結(jié)束囚聚,則停止向手勢(shì)識(shí)別器發(fā)送事件顽铸,僅向hit-test view發(fā)送事件谓松。
  • 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),且此時(shí)觸摸已經(jīng)結(jié)束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對(duì)事件的響應(yīng)拧簸。

cancelsTouchesInView

默認(rèn)為YES男窟。表示當(dāng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)之后歉眷,會(huì)通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng),并不再傳遞事件給hit-test view淑际。若設(shè)置成NO扇住,表示手勢(shì)識(shí)別成功后不取消響應(yīng)鏈對(duì)事件的響應(yīng)艘蹋,事件依舊會(huì)傳遞給hit-test view女阀。

demo中設(shè)置:pan.cancelsTouchesInView = NO

滑動(dòng)時(shí)日志如下:

-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
View panned
View panned
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
...

即便滑動(dòng)手勢(shì)識(shí)別器識(shí)別了手勢(shì),Application也會(huì)依舊發(fā)送事件給YellowView冯键。

delaysTouchesBegan

默認(rèn)為NO琼了。默認(rèn)情況下手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,當(dāng)觸摸狀態(tài)發(fā)生改變時(shí)昧诱,Application都會(huì)將事件傳遞給手勢(shì)識(shí)別器和hit-tested view所袁;若設(shè)置成YES燥爷,則表示手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,截?cái)嗍录膳洌床粫?huì)將事件發(fā)送給hit-tested view道川。

設(shè)置pan.delaysTouchesBegan = YES

日志如下:

View panned
View panned
View panned
View panned
...

因?yàn)榛瑒?dòng)手勢(shì)識(shí)別器在識(shí)別期間立宜,事件不會(huì)傳遞給YellowView橙数,因此期間YellowView的touchesBegan:withEvent:touchesMoved:withEvent:都不會(huì)被調(diào)用;而后滑動(dòng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)崖技,也就獨(dú)吞了事件响疚,不會(huì)再傳遞給YellowView瞪醋。因此只打印了手勢(shì)識(shí)別器成功識(shí)別手勢(shì)后的action調(diào)用银受。

delaysTouchesEnded

默認(rèn)為YES。當(dāng)手勢(shì)識(shí)別失敗時(shí)咕幻,若此時(shí)觸摸已經(jīng)結(jié)束顶霞,會(huì)延遲一小段時(shí)間(0.15s)再調(diào)用響應(yīng)者的touchesEnded:withEvent:;若設(shè)置成NO玄叠,則在手勢(shì)識(shí)別失敗時(shí)會(huì)立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用touchesEnded:withEvent:結(jié)束事件響應(yīng)拓提。

總結(jié):手勢(shì)識(shí)別器比響應(yīng)鏈具有更高的事件響應(yīng)優(yōu)先級(jí)代态。

大師兄—UIControl

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件蹦疑,iOS中UIButton、UISegmentedControl蒋搜、UISwitch等控件都是UIControl的子類判莉。當(dāng)UIControl跟蹤到觸摸事件時(shí)券盅,會(huì)向其上添加的target發(fā)送事件以執(zhí)行action浙于。值得注意的是废菱,UIConotrol是UIView的子類,因此本身也具備UIResponder應(yīng)有的身份花鹅。

關(guān)于UIControl枫浙,此處介紹兩點(diǎn):
1.target-action執(zhí)行時(shí)機(jī)及過程
2.觸摸事件優(yōu)先級(jí)

target-action

  • target:處理交互事件的對(duì)象
  • action:處理交互事件的方式

UIControl作為能夠響應(yīng)事件的控件箩帚,必然也需要待事件交互符合條件時(shí)才去響應(yīng),因此也會(huì)跟蹤事件發(fā)生的過程盔然。不同于UIControl以及UIGestureRecognizer通過touches系列方法跟蹤愈案,UIControl有其獨(dú)特的跟蹤方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

乍一看,這4個(gè)方法和UIResponder的那4個(gè)方法幾乎吻合潦嘶,只不過UIControl只能接收單點(diǎn)觸控掂僵,因此接收的參數(shù)是單個(gè)UITouch對(duì)象顷歌。這幾個(gè)方法的職能也和UIResponder一致眯漩,用來跟蹤觸摸的開始、滑動(dòng)舱卡、結(jié)束轮锥、取消要尔。不過赵辕,UIControl本身也是UIResponder,因此同樣有touches系列的4個(gè)方法饲握。事實(shí)上互拾,UIControl的Tracking系列方法是在touch系列方法內(nèi)部調(diào)用的嚎幸。比如beginTrackingWithTouch是在touchesBegan方法內(nèi)部調(diào)用的嫉晶, 因此它雖然也是UIResponder田篇,但touches系列方法的默認(rèn)實(shí)現(xiàn)和UIResponder本類還是有區(qū)別的泊柬。

當(dāng)UIControl跟蹤事件的過程中兽赁,識(shí)別出事件交互符合響應(yīng)條件冷守,就會(huì)觸發(fā)target-action進(jìn)行響應(yīng)拍摇。UIControl控件通過addTarget:action:forControlEvents:添加事件處理的target和action,當(dāng)事件發(fā)生時(shí)蜂莉,UIControl通知target執(zhí)行對(duì)應(yīng)的action混卵。說是“通知”其實(shí)很籠統(tǒng)映穗,事實(shí)上這里有個(gè)action傳遞的過程。當(dāng)UIControl監(jiān)聽到需要處理的交互事件時(shí)幕随,會(huì)調(diào)用sendAction:to:forEvent:將target蚁滋、action以及event對(duì)象發(fā)送給全局應(yīng)用,Application對(duì)象再通過sendAction:to:from:forEvent:向target發(fā)送action。

target-action過程

因此澄阳,可以通過重寫UIControl的sendAction:to:forEvent:sendAction:to:from:forEvent:自定義事件執(zhí)行的target及action拥知。

另外,若不指定target碎赢,即addTarget:action:forControlEvents:時(shí)target傳空低剔,那么當(dāng)事件發(fā)生時(shí),Application會(huì)在響應(yīng)鏈上從上往下尋找能響應(yīng)action的對(duì)象肮塞。官方說明如下:

If you specifynilfor the target object, the control searches the responder chain for an object that defines the specified action method.

觸摸事件優(yōu)先級(jí)
當(dāng)原本關(guān)系已經(jīng)錯(cuò)綜復(fù)雜的UIGestureRecognizer和UIResponder之間又冒出一個(gè)UIControl襟齿,又會(huì)摩擦出什么樣的火花呢?

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.

簡(jiǎn)單理解:UIControl會(huì)阻止父視圖上的手勢(shì)識(shí)別器行為枕赵,也就是UIControl處理事件的優(yōu)先級(jí)比UIGestureRecognizer高猜欺,但前提是相比于父視圖上的手勢(shì)識(shí)別器。


UIControl測(cè)試場(chǎng)景
  • 預(yù)置場(chǎng)景:在BlueView上添加一個(gè)button拷窜,同時(shí)給button添加一個(gè)target-action事件开皿。
    示例一:在BlueView上添加點(diǎn)擊手勢(shì)識(shí)別器
    示例二:在button上添加手勢(shì)識(shí)別器

  • 操作方式:?jiǎn)螕鬮utton

  • 測(cè)試結(jié)果:示例一中涧黄,button的target-action響應(yīng)了單擊事件;示例二中赋荆,BlueView上的手勢(shì)識(shí)別器響應(yīng)了事件笋妥。過程日志打印如下:

//示例一
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 5
-[CLButton touchesEnded:withEvent:]
-[CLButton endTrackingWithTouch:withEvent:]
按鈕點(diǎn)擊
//示例二
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 3手勢(shì)觸發(fā)
-[CLButton touchesCancelled:withEvent:]
-[CLButton cancelTrackingWithEvent:]
  • 原因分析:點(diǎn)擊button后,事件先傳遞給手勢(shì)識(shí)別器窄潭,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder春宣,這一過程和普通事件響應(yīng)者無異)。示例一中嫉你,由于button阻止了父視圖BlueView中的手勢(shì)識(shí)別器的識(shí)別月帝,導(dǎo)致手勢(shì)識(shí)別器識(shí)別失敗(狀態(tài)為failed 枚舉值為5)均抽,button完全接手了事件的響應(yīng)權(quán)嫁赏,事件最終由button響應(yīng);示例二中油挥,button未阻止其本身綁定的手勢(shì)識(shí)別器的識(shí)別潦蝇,因此手勢(shì)識(shí)別器先識(shí)別手勢(shì)并識(shí)別成功(狀態(tài)為ended 枚舉值為3),而后通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng)深寥,因?yàn)?code>touchesCancelled被調(diào)用攘乒,同時(shí)cancelTrackingWithEvent跟著調(diào)用,因此button的target-action得不到執(zhí)行惋鹅。

  • 其他:經(jīng)測(cè)試则酝,若示例一中的手勢(shì)識(shí)別器設(shè)置cancelsTouchesInView
    為NO,手勢(shì)識(shí)別器和button都能響應(yīng)事件闰集。也就是說這種情況下沽讹,button不會(huì)阻止父視圖中手勢(shì)識(shí)別器的識(shí)別。

  • 結(jié)論:UIControl比其父視圖上的手勢(shì)識(shí)別器具有更高的事件響應(yīng)優(yōu)先級(jí)武鲁。

TODO:上述過程中爽雄,手勢(shì)識(shí)別器在執(zhí)行touchesEnded時(shí)是根據(jù)什么將狀態(tài)置為ended還是failed的?即根據(jù)什么判斷應(yīng)當(dāng)識(shí)別成功還是識(shí)別失斻迨蟆挚瘟?

撥云見日

現(xiàn)在,把膠卷回放到本章節(jié)開頭的場(chǎng)景饲梭。給你一杯咖啡的時(shí)間看看能不能解釋得通那幾個(gè)現(xiàn)象了乘盖,不說了泡咖啡去了...

我肥來了!

先看現(xiàn)象二憔涉,短按 cell無法響應(yīng)订框,日志如下:

-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]

這個(gè)日志和上面離散型手勢(shì)Demo中打印的日志完全一致。短按后兜叨,BackView上的手勢(shì)識(shí)別器先接收到事件布蔗,之后事件傳遞給hit-tested view藤违,作為響應(yīng)者鏈中一員的GLTableView的touchesBegan:withEvent:被調(diào)用;而后手勢(shì)識(shí)別器成功識(shí)別了點(diǎn)擊事件纵揍,action執(zhí)行顿乒,同時(shí)通知Application取消響應(yīng)鏈中的事件響應(yīng),GLTableView的touchesCancelled:withEvent:被調(diào)用泽谨。

因?yàn)槭录蝗∠髓甸虼薈ell無法響應(yīng)點(diǎn)擊。

再看現(xiàn)象三吧雹,長(zhǎng)按cell能夠響應(yīng)骨杂,日志如下:

-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!

長(zhǎng)按的過程中,一開始事件同樣被傳遞給手勢(shì)識(shí)別器和hit-tested view雄卷,作為響應(yīng)鏈中一員的GLTableView的touchesBegan:withEvent:被調(diào)用搓蚪;此后在長(zhǎng)按的過程中,手勢(shì)識(shí)別器一直在識(shí)別手勢(shì)丁鹉,直到一定時(shí)間后手勢(shì)識(shí)別失敗妒潭,才將事件的響應(yīng)權(quán)完全交給響應(yīng)鏈。當(dāng)觸摸結(jié)束的時(shí)候揣钦,GLTableView的touchesEnded:withEvent:被調(diào)用雳灾,同時(shí)Cell響應(yīng)了點(diǎn)擊。

OK冯凹,現(xiàn)在回到現(xiàn)象一谎亩。按照之前的分析,快速點(diǎn)擊cell宇姚,講道理不管是表現(xiàn)還是日志都應(yīng)該和現(xiàn)象二一致才對(duì)匈庭。然而日志僅僅打印了手勢(shì)識(shí)別器的action執(zhí)行結(jié)果。分析一下原因:GLTableView的touchesBegan沒有調(diào)用浑劳,說明事件沒有傳遞給hit-tested view阱持。那只有一種可能,就是事件被某個(gè)手勢(shì)識(shí)別器攔截了呀洲。目前已知的手勢(shì)識(shí)別器攔截事件的方法紊选,就是設(shè)置delaysTouchesBegan為YES啼止,在手勢(shì)識(shí)別器未識(shí)別完成的情況下不會(huì)將事件傳遞給hit-tested view道逗。然后事實(shí)上并沒有進(jìn)行這樣的設(shè)置,那么問題可能出在別的手勢(shì)識(shí)別器上献烦。

Window的sendEvent:打個(gè)斷點(diǎn)查看event上的touch對(duì)象維護(hù)的手勢(shì)識(shí)別器數(shù)組:

ScrollView延遲發(fā)送事件

捕獲可疑對(duì)象:UIScrollViewDelayedTouchesBeganGestureRecognizer滓窍,光看名字就覺得這貨脫不了干系。從類名上猜測(cè)巩那,這個(gè)手勢(shì)識(shí)別器大概會(huì)延遲事件向響應(yīng)鏈的傳遞吏夯。github上找到了該私有類的頭文件

@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {          
    UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client; 
    struct CGPoint { 
        float x; 
        float y; 
    } _startSceneReferenceLocation; 
    UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end

有一個(gè)_touchDelay變量此蜈,大概是用來控制延遲事件發(fā)送的。另外噪生,方法列表里有個(gè)sendTouchesShouldBeginForDelayedTouches:方法裆赵,聽名字似乎是在一段時(shí)間延遲后向響應(yīng)鏈傳遞事件用的。為一探究竟跺嗽,我創(chuàng)建了一個(gè)類hook了這個(gè)方法:

//TouchEventHook.m
+ (void)load{ 
      Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer"); 
      SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:); 
      Method method = class_getClassMethod([self class], sel);   
      class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method)); 
      exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}

- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{ 
[self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}

void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {   
      Method oldMethod = class_getInstanceMethod(aClass, oldSEL);     
      Method newMethod = class_getInstanceMethod(aClass, newSEL); 
      method_exchangeImplementations(oldMethod, newMethod);
}

斷點(diǎn)看一下點(diǎn)擊cell后hook_sendTouchesShouldBeginForDelayedTouches:調(diào)用時(shí)的信息:

延遲的本質(zhì)

可以看到這個(gè)手勢(shì)識(shí)別器的 _touchDelay 變量中战授,保存了一個(gè)計(jì)時(shí)器,以及一個(gè)長(zhǎng)得很像延遲時(shí)間間隔的變量m_delay〗凹蓿現(xiàn)在植兰,可以推測(cè)該手勢(shì)識(shí)別器截?cái)嗔耸录⒀舆t0.15s才發(fā)送給hit-tested view。為驗(yàn)證猜測(cè)璃吧,我分別在Window的sendEvent:楣导,hook_sendTouchesShouldBeginForDelayedTouches:以及TableView的touchesBegan:中打印時(shí)間戳,若猜測(cè)成立畜挨,則應(yīng)當(dāng)前兩者的調(diào)用時(shí)間相差0.15s左右筒繁,后兩者的調(diào)用時(shí)間很接近。短按Cell后打印結(jié)果如下(不能快速點(diǎn)擊朦促,否則還沒過延遲時(shí)間觸摸就結(jié)束了膝晾,無法驗(yàn)證猜測(cè)):

-[GLWindow sendEvent:]調(diào)用時(shí)間戳 :
525252194779.07ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]調(diào)用時(shí)間戳 :
525252194930.91ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]調(diào)用時(shí)間戳 :
525252194931.24ms
-[GLTableView touchesBegan:withEvent:]調(diào)用時(shí)間戳 :
525252194931.76ms

因?yàn)橛袃蓚€(gè)UIScrollViewDelayedTouchesBeganGestureRecognizer,所以hook_sendTouchesShouldBeginForDelayedTouches調(diào)了兩次务冕,兩次的時(shí)間很接近血当。可以看到禀忆,結(jié)果完全符合猜測(cè)臊旭。

這樣就都解釋得通了。現(xiàn)象一由于點(diǎn)擊后箩退,UIScrollViewDelayedTouchesBeganGestureRecognizer攔截了事件并延遲了0.15s發(fā)送离熏。又因?yàn)辄c(diǎn)擊時(shí)間比0.15s短,在發(fā)送事件前觸摸就結(jié)束了戴涝,因此事件沒有傳遞到hit-tested view滋戳,導(dǎo)致TableView的touchBegin沒有調(diào)用。而現(xiàn)象二啥刻,由于短按的時(shí)間超過了0.15s奸鸯,手勢(shì)識(shí)別器攔截了事件并經(jīng)過0.15s后,觸摸還未結(jié)束可帽,于是將事件傳遞給了hit-tested view娄涩,使得TableView接收到了事件。因此現(xiàn)象二的日志雖然和離散型手勢(shì)Demo中的日志一致映跟,但實(shí)際上前者的hit-tested view是在觸摸后延遲了約0.15s左右才接收到觸摸事件的蓄拣。

至于現(xiàn)象四 扬虚,你現(xiàn)在應(yīng)該已經(jīng)覺得理所當(dāng)然了才對(duì)。

總結(jié)

  • 觸摸發(fā)生時(shí)球恤,系統(tǒng)內(nèi)核生成觸摸事件辜昵,先由IOKit處理封裝成IOHIDEvent對(duì)象,通過IPC傳遞給系統(tǒng)進(jìn)程SpringBoard咽斧,而后再傳遞給前臺(tái)APP處理路鹰。
  • 事件傳遞到APP內(nèi)部時(shí)被封裝成開發(fā)者可見的UIEvent對(duì)象,先經(jīng)過hit-testing尋找第一響應(yīng)者收厨,而后由Window對(duì)象將事件傳遞給hit-tested view晋柱,并開始在響應(yīng)鏈上的傳遞。
  • UIRespnder诵叁、UIGestureRecognizer雁竞、UIControl,籠統(tǒng)地講拧额,事件響應(yīng)優(yōu)先級(jí)依次遞增碑诉。

一個(gè)應(yīng)用

在實(shí)際工作中,遇到了一個(gè)觸摸事件的使用侥锦。如果一個(gè)UIView的大小比較小进栽,而點(diǎn)擊區(qū)域又要求比較大,應(yīng)當(dāng)如何實(shí)現(xiàn)呢恭垦?
通過觸摸事件的學(xué)習(xí)快毛,就可以知道如何擴(kuò)大響應(yīng)的區(qū)域,重寫- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event這個(gè)方法就可以實(shí)現(xiàn)番挺。代碼如下:

//
//  WQExtView.h
//  testGes
//

#import <UIKit/UIKit.h>

@interface WQExtView : UIView

/**
 擴(kuò)展響應(yīng)區(qū)域
 設(shè)定值為單側(cè)的px
 上下左右都會(huì)加上這個(gè)擴(kuò)展值
 */
@property (nonatomic, assign) NSInteger extInteractEdge;

/**
 擴(kuò)展響應(yīng)區(qū)域
 */
@property (nonatomic, assign) UIEdgeInsets extInteractInsets;

@end
//
//  WQExtView.m
//  testGes
//

#import "WQExtView.h"

@implementation WQExtView

- (void)setExtInteractEdge:(NSInteger)extInteractEdge
{
    _extInteractEdge = extInteractEdge;
    self.extInteractInsets = UIEdgeInsetsMake(extInteractEdge, extInteractEdge, extInteractEdge, extInteractEdge);
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return CGRectContainsPoint(CGRectMake(self.bounds.origin.x - self.extInteractInsets.left, self.bounds.origin.y - self.extInteractInsets.top, self.bounds.size.width + self.extInteractInsets.left+self.extInteractInsets.right, self.bounds.size.height + self.extInteractInsets.top+self.extInteractInsets.bottom) , point);
}

@end

參考資料

1.史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇
2.Understanding Event Handling, Responders, and the Responder Chain
3.iOS觸摸事件的流動(dòng)
4.UIKit: UIControl

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末唠帝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子玄柏,更是在濱河造成了極大的恐慌襟衰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粪摘,死亡現(xiàn)場(chǎng)離奇詭異瀑晒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)徘意,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門苔悦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人映砖,你說我怎么就攤上這事间坐≡职ぃ” “怎么了邑退?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵竹宋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我地技,道長(zhǎng)蜈七,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任莫矗,我火速辦了婚禮飒硅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘作谚。我一直安慰自己三娩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布妹懒。 她就那樣靜靜地躺著雀监,像睡著了一般。 火紅的嫁衣襯著肌膚如雪眨唬。 梳的紋絲不亂的頭發(fā)上会前,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音匾竿,去河邊找鬼瓦宜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岭妖,可吹牛的內(nèi)容都是我干的临庇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼昵慌,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼苔巨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起废离,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤侄泽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蜻韭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悼尾,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年肖方,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闺魏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俯画,死狀恐怖析桥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤泡仗,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布埋虹,位于F島的核電站,受9級(jí)特大地震影響娩怎,放射性物質(zhì)發(fā)生泄漏搔课。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一截亦、第九天 我趴在偏房一處隱蔽的房頂上張望爬泥。 院中可真熱鬧,春花似錦崩瓤、人聲如沸袍啡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葬馋。三九已至,卻和暖如春肾扰,著一層夾襖步出監(jiān)牢的瞬間畴嘶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工集晚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窗悯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓偷拔,卻偏偏與公主長(zhǎng)得像蒋院,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子莲绰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容