iOS:事件的傳遞和響應(yīng)機(jī)制

事件的生命周期是:

  1. 事件的產(chǎn)生(發(fā)生觸摸等)
  2. 事件的傳遞(找到最適合處理事件的控件)
  3. 事件的響應(yīng)(處理事件)

響應(yīng)者對(duì)象(UIResponder)

在iOS中不是任何對(duì)象都能處理事件振乏,只有繼承了UIResponder的對(duì)象才能接受并處理事件澄干,我們稱之為“響應(yīng)者對(duì)象”。

以下都是繼承自UIResponder的脯丝,所以都能接收并處理事件崭孤。
UIApplication
UIViewController
UIView

那么為什么繼承自UIResponder的類就能夠接收并處理事件呢胀溺?

因?yàn)閁IResponder中提供了以下4個(gè)方法來(lái)處理觸摸事件厘唾。

// 開(kāi)始觸摸
// 若是兩根手指同時(shí)觸摸,view調(diào)用一次touchesBegan方法鹃操,且touches內(nèi)包含兩個(gè)touchu對(duì)象
// 若是兩根手指一前一后觸摸韭寸,view調(diào)用兩次次touchesBegan方法,且touches只包含一個(gè)touchu對(duì)象
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//開(kāi)始移動(dòng)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//結(jié)束觸摸荆隘,離開(kāi)view
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//結(jié)束觸摸錢恩伺,某個(gè)系統(tǒng)事件(例如電話呼入)會(huì)打斷觸摸過(guò)程巾钉,系統(tǒng)會(huì)自動(dòng)調(diào)用cancel方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

以上四個(gè)方法是由系統(tǒng)自動(dòng)調(diào)用的牵啦,可以通過(guò)重寫該方法來(lái)處理一些事件吗冤。

重寫這些方法必須是自定義view的類中實(shí)現(xiàn)柬甥,因?yàn)橄到y(tǒng)沒(méi)有給我們UIView.m的源碼,我們只能通過(guò)子類繼承父類實(shí)現(xiàn)叶组。(此處強(qiáng)調(diào)的是UIView壳繁,不是UIViewCOntroller)

重寫UIViewController的觸摸事件坟募,可以直接在控制器的.m文件中重寫這些方法即可缆毁。

UIRespond還有加速計(jì)事件番川、遠(yuǎn)程控制事件的處理,此處不展開(kāi)描述。

加速計(jì)事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠(yuǎn)程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

UITouch對(duì)象

當(dāng)用戶手指觸摸屏幕的時(shí)候爽彤,會(huì)創(chuàng)建一個(gè)與手指相關(guān)的Touch對(duì)象。

作用:
1缚陷、保存著跟手指相關(guān)的信息适篙,比如觸摸的位置、時(shí)間箫爷、階段等嚷节。
2、當(dāng)手指移動(dòng)時(shí)虎锚,系統(tǒng)會(huì)更新同一個(gè)UITouch對(duì)象硫痰,使之能夠一直保存該手指在的觸摸位置。
3窜护、當(dāng)手指離開(kāi)屏幕時(shí)效斑,系統(tǒng)會(huì)銷毀相應(yīng)的UITouch對(duì)象。

事件的產(chǎn)生

當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后柱徙,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中缓屠。

為什么是隊(duì)列而不是棧?
因?yàn)殛?duì)列的特點(diǎn)是FIFO护侮,即先進(jìn)先出敌完,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊(duì)列羊初。

事件的傳遞

事件發(fā)生并加入到UIApplication管理的事件隊(duì)列中后滨溉,UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理长赞,一般是先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)晦攒。主窗口會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件。

觸摸事件的傳遞是從父控件傳遞到子控件
也就是UIApplication->window->尋找處理事件最合適的view

注意:如果父控件不能接受觸摸事件得哆,那么子控件就不可能接收到觸摸事件

如何找到最合適的控件來(lái)處理事件

1勤家、首先判斷主窗口(keyWindow)自己是否能接受觸摸事件

不能接受觸發(fā)事件的原因:
1、不允許交互:userInteractionEnabled = NO
2柳恐、隱藏:如果把父控件隱藏伐脖,那么子控件也會(huì)隱藏,隱藏的控件不能接受事件
3乐设、透明度:如果設(shè)置一個(gè)控件的透明度<0.01讼庇,會(huì)直接影響子控件的透明度。alpha:0.0~0.01為透明近尚。

注意:默認(rèn)UIImageView不能接受觸摸事件蠕啄,因?yàn)椴辉试S交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互歼跟,需要設(shè)置UIImageView的userInteractionEnabled = YES和媳。

2、判斷觸摸點(diǎn)是否在自己身上哈街。

3留瞳、子控件數(shù)組中從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟骚秦。

所謂從后往前遍歷子控件她倘,就是首先查找子控件數(shù)組中最后一個(gè)元素,然后執(zhí)行1作箍、2步驟硬梁,(采取倒序遍歷子控件的方式尋找最合適的view是為了做一些循環(huán)優(yōu)化,因?yàn)橄啾容^之下胞得,后添加的view在上面荧止,降低循環(huán)次數(shù)。)

4阶剑、view罩息,比如叫做hitView,那么會(huì)把這個(gè)事件交給這個(gè)hitView个扰,再遍歷這個(gè)hitView的子控件瓷炮,直至沒(méi)有更合適的view為止。
5递宅、如果沒(méi)有符合條件的子控件娘香,那么就認(rèn)為自己最合適處理這個(gè)事件,也就是自己是最合適的view办龄。

兩個(gè)重要的方法:

1烘绽、-(nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

只要事件一傳遞給一個(gè)控件,這個(gè)控件就會(huì)調(diào)用它自己的hitTest:withEvent:方法,目的是返回最適合的view俐填。

可以通過(guò)重寫這個(gè)方法安接,返回指定的view作為最合適的view。

如果hitTest:withEvent:方法中返回nil英融,那么調(diào)用該方法的控件本身和其子控件都不是最合適的view盏檐,也就是在自己身上沒(méi)有找到更合適的view。那么最合適的view就是該控件的父控件驶悟。

事件的傳遞順序是這樣的:

產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view胡野。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判斷下窗口能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;
    // 2.判斷下點(diǎn)在不在窗口上
    // 不在窗口上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 設(shè)置一個(gè)初始視圖
    __block UIView *hitView = nil;
    // 遍歷子視圖
    // NSEnumerationReverse 倒序
    [self.subviews enumerateObjectsWithOptions: NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
          
        CGPoint p = [self convertPoint:point toView:obj];
          
        hitView = [obj hitTest:p withEvent:event];
          
        if (hitView) {
            *stop = YES;
        }
          
    }];
          
    if (hitView){
        return  hitView;
    }
    // 4.沒(méi)有找到更合適的view,也就是沒(méi)有比自己更合適的view
    return self;
}

2痕鳍、-(BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

判斷點(diǎn)在不在當(dāng)前view上(方法調(diào)用者的坐標(biāo)系上)如果返回YES硫豆,代表點(diǎn)在方法調(diào)用者的坐標(biāo)系上龙巨;返回NO代表點(diǎn)不在方法調(diào)用者的坐標(biāo)系上,那么方法調(diào)用者也就不能處理事件熊响。

如果要修改點(diǎn)擊區(qū)域旨别,核心方法就是修改上述兩個(gè)方法。通過(guò)修改這兩個(gè)方法汗茄,根據(jù)需求修改響應(yīng)區(qū)域范圍即可秸弛。

事件傳遞流程圖

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 內(nèi)部實(shí)現(xiàn)流程圖

先校驗(yàn)view是不是隱藏,是不是可以觸發(fā)點(diǎn)擊剔难,是不是透明度大于0.01胆屿,條件全部滿足就繼續(xù)校驗(yàn)奥喻,否則是返回到父視圖繼續(xù)遍歷調(diào)用其兄弟視圖偶宫。

再校驗(yàn)點(diǎn)是不是在此view上,是的話開(kāi)始倒序遍歷自己的子視圖环鲤,不是的話纯趋,也會(huì)返回到父視圖,繼續(xù)遍歷其兄弟視圖冷离。

最終若找到就返回view吵冒,沒(méi)找到就返回nil。

事件響應(yīng)

事件從UIApplication->UIWindow->尋找處理事件最合適的view傳遞后
然后就會(huì)調(diào)用控件的touches方法來(lái)作具體的事件處touchesBegan…touchesMoved…touchedEnded…等西剥。
touches 方法就是對(duì)事件的響應(yīng)痹栖,事件的響應(yīng)是順著響應(yīng)鏈向上傳遞的,這個(gè)傳遞是依賴于UIResponder的nextResponder瞭空。

UIView :如果view是VC的root view揪阿,則它的nextResponder是 VC;否則是父view
UIViewController :如果 vc 是window的root vc咆畏,則它的nextResponder是 window南捂,否則是父vc
UIWindow:它的nextResponder是UIApplication
UIApplication :它的nextResponder是app delegate。

整個(gè)事件在找到合適的view之后旧找,判斷當(dāng)前view是否能處理這個(gè)事件溺健,如果不能,則順著nextResponder向父view傳遞钮蛛,如果傳遞到VC也不能處理這個(gè)事件鞭缭,則繼續(xù)傳遞到UIWindow,如果window對(duì)象也不處理魏颓,則其將事件或消息傳遞給UIApplication對(duì)象缚去,如果UIApplication也不能處理該事件或消息,則將其丟棄琼开。其中任何一環(huán)能處理事件易结,則進(jìn)行時(shí)間處理,整個(gè)事件的傳遞就結(jié)束了。


視圖事件響應(yīng)
-(void)touchesBegan:(NSSet*)touches withEvent:(UIevent *)event;
-(void)touchesMoved:(NSSet*)touches withEvent:(UIevent *)event;
-(void)touchesended:(NSSet*)touches withEvent:(UIevent *)event;

總結(jié)

事件處理的整個(gè)流程總結(jié):

  1. 觸摸屏幕產(chǎn)生觸摸事件后搞动,觸摸事件會(huì)被添加到由UIApplication管理的事件隊(duì)列中(即躏精,首先接收到事件的是UIApplication)。
  2. UIApplication會(huì)從事件隊(duì)列中取出最前面的事件鹦肿,把事件傳遞給應(yīng)用程序的主窗口(keyWindow)矗烛。
  3. key window會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件。(至此箩溃,第一步已完成)
  4. 最合適的view會(huì)調(diào)用自己的touches方法處理事件瞭吃。
  5. touches默認(rèn)做法是把事件順著響應(yīng)者鏈條向上拋,即順著nextResponder向上傳遞涣旨。

事件的傳遞和響應(yīng)的區(qū)別:

事件的傳遞是從上到下(父控件到子控件)歪架。

事件的響應(yīng)是從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件)。

應(yīng)用實(shí)踐

1霹陡、指定區(qū)域響應(yīng)事件

僅紅色區(qū)域內(nèi)可以點(diǎn)擊

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 計(jì)算點(diǎn)擊位置與按鈕中心點(diǎn)的距離
    CGFloat distance = sqrt(pow(point.x - CGRectGetMidX(self.bounds), 2) + pow(point.y - CGRectGetMidY(self.bounds), 2));
    // 判斷距離是否小于圓形半徑 
    CGFloat radius = MIN(self.bounds.size.width, self.bounds.size.height) * 0.4;
    return distance <= radius;
}
- 具體的判斷條件根據(jù)需求設(shè)定和蚪,還可以擴(kuò)大/縮小點(diǎn)擊范圍

2、表述下圖事件傳遞和響應(yīng)流程

傳遞過(guò)程:

屏幕點(diǎn)擊這個(gè)位置烹棉,這個(gè)事件傳遞UIApplication
UIApplication傳遞給UIWindow(UIWindow也是一個(gè)視圖)
從UIWindow里面開(kāi)始執(zhí)行hitTest方法查找最適合的視圖攒霹。
hitTest方法內(nèi)部調(diào)用pointInside方法判斷點(diǎn)擊的位置是不是在視圖范圍內(nèi)
如果是,
則倒序遍歷子視圖浆洗,(后添加的view在上面催束,降低循環(huán)次數(shù)),返回合適的view嗎伏社,再在此view及其子視圖繼續(xù)調(diào)用hitTest方法抠刺,直到找到最適合的view。
如果不是洛口,返回UIWindow矫付。

根據(jù)圖片標(biāo)注的view名稱:

ViewA->ViewB2->ViewD->ViewD中白色區(qū)域

響應(yīng)過(guò)程:

找到最適合的view后,開(kāi)始調(diào)用touches事件
touches默認(rèn)做法是把事件順著響應(yīng)鏈條向上拋第焰,也就是順著nextResponder向上傳遞买优。
結(jié)合圖中名稱:
點(diǎn)擊的白色區(qū)域有ViewD響應(yīng)
若ViewD不響應(yīng)則由其父視圖ViewB2去響應(yīng)
若ViewB2不響應(yīng)則由其父視圖ViewA去響應(yīng)
若ViewA不響應(yīng)則順著nextResponder繼續(xù)查找直到UIApplication->UIApplicationDelegate。

如果整個(gè)過(guò)程中都沒(méi)有響應(yīng)挺举,則忽略此事件杀赢,app不會(huì)任何反應(yīng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末湘纵,一起剝皮案震驚了整個(gè)濱河市脂崔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梧喷,老刑警劉巖砌左,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脖咐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡汇歹,警方通過(guò)查閱死者的電腦和手機(jī)屁擅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)产弹,“玉大人派歌,你說(shuō)我怎么就攤上這事√瞪冢” “怎么了胶果?”我有些...
    開(kāi)封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)斤斧。 經(jīng)常有香客問(wèn)我早抠,道長(zhǎng),這世上最難降的妖魔是什么折欠? 我笑而不...
    開(kāi)封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任贝或,我火速辦了婚禮吼过,結(jié)果婚禮上锐秦,老公的妹妹穿的比我還像新娘。我一直安慰自己盗忱,他們只是感情好酱床,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著趟佃,像睡著了一般扇谣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闲昭,一...
    開(kāi)封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天罐寨,我揣著相機(jī)與錄音,去河邊找鬼序矩。 笑死鸯绿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的簸淀。 我是一名探鬼主播瓶蝴,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼租幕!你這毒婦竟也來(lái)了舷手?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤劲绪,失蹤者是張志新(化名)和其女友劉穎男窟,沒(méi)想到半個(gè)月后盆赤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歉眷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年弟劲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姥芥。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兔乞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凉唐,到底是詐尸還是另有隱情庸追,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布台囱,位于F島的核電站淡溯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏簿训。R本人自食惡果不足惜咱娶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望强品。 院中可真熱鬧膘侮,春花似錦、人聲如沸的榛。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)夫晌。三九已至雕薪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晓淀,已是汗流浹背所袁。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凶掰,地道東北人燥爷。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像锄俄,于是被迫代替她去往敵國(guó)和親局劲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

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