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

這里主要講解記錄下用戶觸摸點(diǎn)擊手機(jī)屏幕后產(chǎn)生的事件是如何派發(fā)傳遞的,如何查找到適合響應(yīng)事件的第一響應(yīng)者控件量九,以及找到響應(yīng)者后事件是如何通過響應(yīng)鏈向下傳遞的,直到事件被接收并做出具體處理或被廢棄舷丹。事件響應(yīng)鏈也是面試中經(jīng)常會(huì)被問起的知識(shí)點(diǎn)涮毫!

一、相關(guān)概念

  • 第一響應(yīng)者:

第一響應(yīng)者一般指的是用戶當(dāng)前觸摸的響應(yīng)者對(duì)象呢铆,表示當(dāng)前該對(duì)象正在與用戶交互晦鞋,第一響應(yīng)者是響應(yīng)者鏈的開端。

響應(yīng)者鏈和事件分發(fā)傳遞的使命都是找出第一響應(yīng)者

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

具有響應(yīng)和處理iOS事件能力的對(duì)象棺克,也就是繼承UIResponder的類的對(duì)象悠垛。我們常用的UIApplication、UIWindow娜谊、UIViewController确买、UIView、UIScene(iOS13以后)都是繼承或間接UIResponder類纱皆,所以他們的實(shí)例對(duì)象都可以成為響應(yīng)者對(duì)象湾趾。

類的繼承關(guān)系:

圖片
  • 響應(yīng)者鏈:

由多個(gè)不同響應(yīng)者對(duì)象鏈接起來構(gòu)成的一個(gè)鏈條;

響應(yīng)者鏈可以看做是鏈表派草,整體是一個(gè)樹搀缠,因?yàn)槊總€(gè)節(jié)點(diǎn)都是一個(gè)響應(yīng)者對(duì)象,每個(gè)響應(yīng)者對(duì)象都存有指向下一個(gè)響應(yīng)者的指針nextResponder,可以通過nestResponder找到下一個(gè)responder近迁,直到找到第一響應(yīng)者響應(yīng)了事件就會(huì)停止傳遞艺普,如果最終沒有響應(yīng)者響應(yīng)事件,那么該事件就會(huì)被廢棄鉴竭。

二歧譬、iOS中的事件類型:

iOS中事件主要分為三大類:

1、Touch Event (觸摸事件)

解釋:用戶觸摸屏幕產(chǎn)生的交互事件

2搏存、Motion Event (運(yùn)動(dòng)事件)

解釋:運(yùn)動(dòng)事件也叫做加速計(jì)事件缴罗,這類事件是依賴手機(jī)里的加速計(jì)、陀螺儀等硬件傳感器實(shí)現(xiàn)的祭埂。用戶在搖晃手機(jī)面氓、傾斜手機(jī)的售后就會(huì)產(chǎn)生這類事件兵钮。可用于屏幕轉(zhuǎn)屏監(jiān)控舌界。

3掘譬、Remote-ControlEvent(遠(yuǎn)程控制事件)

解釋:這個(gè)事件指的是用戶在操作多媒體的時(shí)候產(chǎn)生的事件。例如播放音樂時(shí)后臺(tái)播放控制

三呻拌、如何控制控件能不能響應(yīng)事件:

  1. 設(shè)置不允許交互:設(shè)置控件的userInteractionEnabled = NO葱轩;
  2. 設(shè)置控件隱藏:將控件的hidden設(shè)置為Yes隱藏控件;
  3. 設(shè)置透明度:設(shè)置控件的透明度alpha<0.01,放alpha的值在0.0~0.01之間時(shí)控件為透明藐握;
  4. 超出父控件響應(yīng)區(qū)域

注意:如果view被設(shè)置為透明靴拱,那么會(huì)直接影響其子View的透明度;如果view無法響應(yīng)事件那么這個(gè)view上的所有SubView都不可響應(yīng)事件猾普,也就是如果父控件不能接受觸摸事件袜炕,那么子控件就不可能接收到觸摸事件。

四初家、事件的產(chǎn)生和分發(fā)傳遞:

測(cè)試展示圖

圖片
  • 事件是如何產(chǎn)生的偎窘?

當(dāng)用戶觸摸屏幕時(shí),系統(tǒng)會(huì)檢測(cè)到屏幕上的壓力感知到觸摸事件溜在,iOS系統(tǒng)檢測(cè)到觸摸操作后會(huì)將這個(gè)事件打包成一個(gè)UIEvent對(duì)象陌知,并將該事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中,然后UIApplication會(huì)從事件隊(duì)列中取出觸摸事件并傳遞給UIWindow處理掖肋,keyWindow會(huì)使用hitTest:withEvent:方法尋找一個(gè)最合適的響應(yīng)者來處理事件仆葡,一般尋找到的適合處理事件的控件是touch操作初始點(diǎn)的視圖,找到合適第一響應(yīng)者的視圖控件后志笼,就會(huì)調(diào)用該視圖控件的touches方法來處理具體的事件浙芙,這個(gè)過程稱之為hit-test。

  • 處理事件的方法:
UIResponder內(nèi)部提供了以下方法來處理事件觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
  • 事件是如何傳遞的籽腕?

事件的傳遞是由父控件向子控件傳遞的,例如上面的view層次圖,viewA纸俭、viewB皇耗、viewE被添加到rootView中,viewC揍很、viewD是viewB的子view郎楼。加入用戶點(diǎn)擊viewC的時(shí)間傳遞鏈?zhǔn)?/p>

圖片

傳遞方向:由底層系統(tǒng)向可以響應(yīng)事件的控件傳遞

UIKit→UIApplication的事件隊(duì)列→keyWindow→rootView→一些列subView→事件響應(yīng)view

  • 如何查找到合適的事件第一響應(yīng)者?

主要方法:

- (nullable UIView )hitTest:(CGPoint)point withEvent:(nullable UIEvent )event;

注:只要事件傳遞給一個(gè)控件窒悔,那么這個(gè)控件就會(huì)調(diào)用自己的hitTest:withEvent:方法呜袁。他的作用是尋找并返回適合響應(yīng)處理事件的第一響應(yīng)者。

- (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)用者也就不能處理事件虹钮。

流程圖:

圖片
  1. 主窗口接收到應(yīng)用程序傳遞過來的事件后,首先判斷自己能否接手觸摸事件膘融。如果能,那么在判斷觸摸點(diǎn)在不在窗口的范圍內(nèi)
  2. 如果觸摸點(diǎn)也在窗口身上,那么窗口會(huì)從后往前遍歷自己的子控件馁害,遍歷自己的子控件只是為了尋找出來最合適的view褐缠;
  3. 遍歷到每一個(gè)子控件后,又會(huì)重復(fù)上面的兩個(gè)步驟岛都。將傳遞事件給子控件律姨,先判斷子控件能否接受事件,再判斷觸摸點(diǎn)在不在子控件的范圍中臼疫;
  4. 如此循環(huán)遍歷子控件择份,直到找出合適響應(yīng)事件的第一響應(yīng)者,如果沒有更合適的子控件多矮,那么自己就成為最合適的view缓淹。
  • hitTest:withEvent方法中如何處理的?
  1. 首先判斷當(dāng)前視圖是否可響應(yīng)事件塔逃,也就是判斷當(dāng)前視圖的是否可交互狀態(tài)讯壶、隱藏狀態(tài)、透明度湾盗;
  2. 如果當(dāng)前視圖允許響應(yīng)觸摸事件伏蚊,則調(diào)用當(dāng)前視圖的 pointInside:withEvent: 方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi) ;
  3. 若pointInside:withEvent:返回NO格粪,則 hitTest:withEvent: 返回 nil 躏吊;
  4. 若pointInside:withEvent:返回 YES,則向當(dāng)前視圖的所有子視圖發(fā)送 hitTest:withEvent: 消息帐萎,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖比伏,即從 subviews 數(shù)組的末尾向前遍歷 ,直到有子視圖返回非空對(duì)象或者全部子視圖遍歷完畢疆导;
  5. 若第一次有子視圖返回非空對(duì)象赁项,則 hitTest:withEvent: 方法返回此對(duì)象,處理結(jié)束澈段;如所有子視圖都返回 nil悠菜,則 hitTest:withEvent: 方法返回該視圖自身 ;
  6. 找到合適的第一響應(yīng)者后败富,就會(huì)調(diào)用該控件的touches系列方法處理具體的事件悔醋,如果找不到第一響應(yīng)者就不會(huì)調(diào)用touches方法
  • 查找響應(yīng)者實(shí)例

以測(cè)試展示圖為例,假設(shè)用戶點(diǎn)擊viewC后的處理流程

  1. rootView為window的根視圖兽叮,窗口會(huì)首先對(duì)rootView進(jìn)行hit-Test,判斷結(jié)果為用戶點(diǎn)擊位置在rootView的范圍內(nèi)芬骄;
  2. 繼續(xù)檢測(cè)rootView的子控件(viewA,viewB,viewE)相應(yīng)的調(diào)用自己的hit-Test方法猾愿,檢測(cè)到viewA、viewE的pointInside:withEvent:返回NO,則點(diǎn)擊范圍不在viewA德玫、viewE內(nèi)匪蟀,對(duì)應(yīng)的hitTest:withEvent:返回nil,這時(shí)rootView繼續(xù)檢測(cè)viewB的hit-Test方法宰僧,viewB的pointInside:withEvent:返回YES材彪,確定點(diǎn)擊范圍在viewB內(nèi);
  3. 這時(shí)viewB內(nèi)存在viewC和viewD兩個(gè)子控件琴儿,viewD在viewC之后添加到viewB的subViews中段化,因此優(yōu)先檢測(cè)viewD的hit-Test方法,viewD的pointInside:withEvent:返回NO,對(duì)應(yīng)的hitTest:withEvent:返回nil造成,說明點(diǎn)擊不在viewD內(nèi)显熏,viewD及其子控件都不可響應(yīng)事件。因此需要回溯檢測(cè)viewC的hit-Test方法晒屎;
  4. viewC的pointInside:withEvent:返回YES,說明點(diǎn)擊范圍在viewC范圍內(nèi)喘蟆,由于viewC沒有子控件,也可以理解為viewC的子控件hit-Test返回了nil鼓鲁;
  5. 因此viewC的hitTest:withEvent:將會(huì)返回viewC蕴轨,viewB的hitTest:withEvent:返回viewC,rootViewhitTest:withEvent:將會(huì)返回viewC骇吭;
  6. 至此橙弱,本次點(diǎn)擊事件的第一響應(yīng)者就通過響應(yīng)者鏈的事件分發(fā)邏輯找到了

注意:如果最終hit-test沒有找到第一響應(yīng)者,或者第一響應(yīng)者沒有處理該事件燥狰,則該事件會(huì)沿著響應(yīng)者鏈向上回溯棘脐,如果UIWindow實(shí)例和UIApplication實(shí)例都不能處理該事件,則該事件會(huì)被丟棄龙致;

  • hitTest:withEvent:方法底層實(shí)現(xiàn)
// 什么時(shí)候調(diào)用:只要事件一傳遞給一個(gè)控件蛀缝,那么這個(gè)控件就會(huì)調(diào)用自己的這個(gè)方法
// 作用:尋找并返回最合適的view
// UIApplication -> [UIWindow hitTest:withEvent:]尋找最合適的view告訴系統(tǒng)
// point:當(dāng)前手指觸摸的點(diǎn)
// point:是方法調(diào)用者坐標(biāo)系上的點(diǎ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)在不在窗口上 
    // 不在窗口上 
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    // 3.從后往前遍歷子控件數(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)轉(zhuǎn)換為子控件上的點(diǎn) 
    // 把自己控件上的點(diǎn)轉(zhuǎn)換成子控件上的點(diǎn) 
    CGPoint childP = [self convertPoint:point toView:childView]; 
    UIView *fitView = [childView hitTest:childP withEvent:event]; 
    if (fitView) {
    // 如果能找到最合適的view 
    return fitView; 
    }
    } 
    // 4.沒有找到更合適的view,也就是沒有比自己更合適的view 
    return self;
    }

五目代、事件響應(yīng)

  • 響應(yīng)鏈的傳遞方向

由是第一響應(yīng)者的控件向系統(tǒng)傳遞

事件響應(yīng)view→superView→rootVIew→viewController→window→Application→AppDelegate

  • 響應(yīng)者鏈的關(guān)系圖:
圖片

解釋說明:
1屈梁、響應(yīng)者鏈?zhǔn)怯啥鄠€(gè)響應(yīng)者對(duì)象構(gòu)成的鏈條,每個(gè)響應(yīng)者對(duì)象必須是繼承UIResponder類的子類像啼;
2、如果View是控制器VC的View,那么VC就是view的nextUIResponder潭苞;
3忽冻、如果View不是控制器VC的View,那么此View的superView為當(dāng)前view的nextUIResponder;
4此疹、視圖控制器VC的nextUIResponder是控制器view(VC.View)的superView僧诚,即VC.nextUIResponder = VC.View.superView,如下圖;
5遮婶、如果在視圖層都不能處理事件,則將事件傳遞個(gè)UIWindow進(jìn)行處理;
6湖笨、Window的nextResponder是UIApplication旗扑,如果window也不處理事件,則將事件傳遞給UIApplication;
7慈省、UIApplication的nextResponder是AppDelegate,如果UIApplication也不能處理該事件臀防,則將此事件丟棄

說明示圖:

輸出的log 顯示VC.nextUIResponder = VC.View.superView

圖片

六、總結(jié)

  1. 當(dāng)用戶點(diǎn)擊頁(yè)面上一個(gè)view的時(shí)候边败,系統(tǒng)只是檢測(cè)到用戶點(diǎn)擊觸摸了屏幕袱衷,而此時(shí)無法確認(rèn)用戶觸摸的view控件,因此需要根據(jù)事件分發(fā)傳遞的邏輯尋找到可以響應(yīng)事件的第一響應(yīng)者控件;
  2. 如果需要處理特殊的需求笑窜,例如單擊不規(guī)則按鈕事件致燥、點(diǎn)擊事件穿透等問題時(shí)可以重寫主要方法hitTest:withEvent:和pointInside:withEvent:來處理;
  3. 發(fā)生了觸摸或其他事件后,系統(tǒng)將事件打包成UiEvent發(fā)送到UIApplication管理的事件隊(duì)列中排截,UIApplication從隊(duì)列中取出最前面的事件分發(fā)下去;
  4. 如果找到了合適處理事件的控件,會(huì)調(diào)用此控件的touchs系列方法嫌蚤,如果響應(yīng)事件的控件調(diào)用了 super touchs等方法,那么事件會(huì)沿著響應(yīng)鏈向下傳遞断傲,傳遞給下一個(gè)響應(yīng)者脱吱,這個(gè)響應(yīng)者來調(diào)用touchs系列方法;
  5. 如果父視圖不接收處理事件艳悔,那么他的子視圖也不能接收到急凰;
  6. 事件傳遞是由父控件向子控件傳遞的,事件響應(yīng)是由子控件向父控件出啊低的猜年;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抡锈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子乔外,更是在濱河造成了極大的恐慌床三,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杨幼,死亡現(xiàn)場(chǎng)離奇詭異撇簿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)差购,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門四瘫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欲逃,你說我怎么就攤上這事找蜜。” “怎么了稳析?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵洗做,是天一觀的道長(zhǎng)弓叛。 經(jīng)常有香客問我,道長(zhǎng)诚纸,這世上最難降的妖魔是什么撰筷? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮畦徘,結(jié)果婚禮上毕籽,老公的妹妹穿的比我還像新娘。我一直安慰自己旧烧,他們只是感情好影钉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掘剪,像睡著了一般平委。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上夺谁,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天廉赔,我揣著相機(jī)與錄音,去河邊找鬼匾鸥。 笑死蜡塌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勿负。 我是一名探鬼主播馏艾,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奴愉!你這毒婦竟也來了琅摩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤锭硼,失蹤者是張志新(化名)和其女友劉穎房资,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檀头,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡轰异,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暑始。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搭独。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖廊镜,靈堂內(nèi)的尸體忽然破棺而出牙肝,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布惊奇,位于F島的核電站,受9級(jí)特大地震影響播赁,放射性物質(zhì)發(fā)生泄漏颂郎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一容为、第九天 我趴在偏房一處隱蔽的房頂上張望乓序。 院中可真熱鬧,春花似錦坎背、人聲如沸替劈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)陨献。三九已至,卻和暖如春懂更,著一層夾襖步出監(jiān)牢的瞬間眨业,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工沮协, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留龄捡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓慷暂,卻偏偏與公主長(zhǎng)得像聘殖,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子行瑞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353