【iOS面試糧食】UI視圖—iOS事件的傳遞機(jī)制

本文章將記錄有關(guān)iOS事件的傳遞機(jī)制,如有錯(cuò)誤歡迎指出~

iOS的事件分為3大類型

  • Touch Events(觸摸事件)

  • Motion Events(運(yùn)動(dòng)事件夏伊,比如重力感應(yīng)和搖一搖等)

  • Remote Events(遠(yuǎn)程事件摇展,比如用耳機(jī)上得按鍵來控制手機(jī))

在開發(fā)中,最常用到的就是Touch Events(觸摸事件)溺忧,基本貫穿于每個(gè)App中咏连,也是本文的豬腳~ 因此文中所說事件均特指觸摸事件。

接下來鲁森,記錄祟滴、涉及的問題大致包括:

  • 事件是怎么找它的媽媽的?(尋找事件的最佳響應(yīng)者)

  • 事件又是如何去到媽媽的身邊的歌溉?媽媽又將如何對待它垄懂?(事件的響應(yīng)及在響應(yīng)鏈中的傳遞)

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

當(dāng)我們觸摸屏幕的某個(gè)可響應(yīng)的功能點(diǎn)后,最終都會由UIView或者繼承UIView的控件來響應(yīng)

那我們先來看下UIView的兩個(gè)方法:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回尋找到的最終響應(yīng)這個(gè)事件的視圖
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
?
// default returns YES if point is in bounds
//判斷某一個(gè)點(diǎn)擊的位置是否在視圖范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

每個(gè)UIView對象都有一個(gè) hitTest: withEvent: 方法痛垛,這個(gè)方法是Hit-Testing過程中最核心的存在草慧,其作用是詢問事件在當(dāng)前視圖中的響應(yīng)者,同時(shí)又是作為事件傳遞的橋梁匙头。

看看它是什么時(shí)候被調(diào)用的

流程圖
  • 當(dāng)手指接觸屏幕漫谷,UIApplication接收到手指的觸摸事件之后,就會去調(diào)用UIWindowhitTest: withEvent:方法

  • hitTest: withEvent:方法中會調(diào)用pointInside: withEvent:去判斷當(dāng)前點(diǎn)擊的point是否屬于UIWindow范圍內(nèi)蹂析,如果是舔示,就會以倒序的方式遍歷它的子視圖碟婆,即越后添加的視圖,越先遍歷

  • 子視圖也調(diào)用自身的hitTest: withEvent:方法惕稻,來查找最終響應(yīng)的視圖

再來看個(gè)示例:

Hit-testing

視圖層級如下(同一層級的視圖越在下面竖共,表示越后添加):

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

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

事件傳遞順序圖
  • UIWindow將事件傳遞給其子視圖A

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

  • C判斷自身能響應(yīng)事件宋梧,繼續(xù)將事件傳遞給F(同理F比E后添加)匣沼。

  • F判斷自身不能響應(yīng)事件,C又將事件傳遞給E捂龄。

  • E判斷自身能響應(yīng)事件释涛,同時(shí)E已經(jīng)沒有子視圖,因此最終E就是最佳響應(yīng)者倦沧。

以上唇撬,就是尋找最佳響應(yīng)者的整個(gè)過程。

接下來展融,來看下hitTest: withEvent:方法里窖认,都做些了什么?

我們已經(jīng)知道事件在響應(yīng)者之間的傳遞告希,是視圖通過判斷自身能否響應(yīng)事件來決定是否繼續(xù)向子視圖傳遞扑浸,那么判斷響應(yīng)的條件是什么呢?

視圖響應(yīng)事件的條件:

  • 允許交互: userInteractionEnabled = YES

  • 禁止隱藏:hidden = NO

  • 透明度:alpha > 0.01

  • 觸摸點(diǎn)的位置:通過 pointInside: withEvent:方法判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)

代碼的表現(xiàn)大概如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //3種狀態(tài)無法響應(yīng)事件
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    
     //觸摸點(diǎn)若不在當(dāng)前視圖上則無法響應(yīng)事件
    if ([self pointInside:point withEvent:event]) {
         //從后往前遍歷子視圖數(shù)組
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            // 坐標(biāo)系的轉(zhuǎn)換,把觸摸點(diǎn)在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
             //詢問子視圖層級中的最佳響應(yīng)視圖
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                 //如果子視圖中有更合適的就返回
                return hitTestView;
            }
        }
         //沒有在子視圖中找到更合適的響應(yīng)視圖燕偶,那么自身就是最合適的
        return self;
    }
    
    return nil;
}

說了這么多喝噪,那我們可以運(yùn)用hitTest: withEvent:來搞些什么事情呢

使超出父視圖坐標(biāo)范圍的子視圖也能響應(yīng)事件

Demo示意圖

視圖層級如下:

A
├── B

如上圖所示,視圖B有一部分是不在父視圖A的坐標(biāo)范圍內(nèi)的指么,當(dāng)我們觸摸視圖B的上半部分酝惧,是不會響應(yīng)事件的。當(dāng)然伯诬,我們可以通過重寫視圖AhitTest: withEvent:方法來解決這個(gè)需求晚唇。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    //如果找不到合適的響應(yīng)者
    if (view == nil) {
        //視圖B坐標(biāo)系的轉(zhuǎn)換
        CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
        if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
            // 滿足條件,返回視圖B
            view = self.deleteButton;
        }
    }
    
    return view;
}

視圖AhitTest: withEvent:方法中判斷觸摸點(diǎn),是否位于視圖B的視圖范圍內(nèi)姑廉,如果屬于缺亮,則返回視圖B翁涤。這樣一來桥言,當(dāng)我們點(diǎn)擊視圖B的任何位置都可以響應(yīng)事件了萌踱。

注:文章底部有簡單的Demo(僅供參考)

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

經(jīng)歷Hit-Testing后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了号阿,接下來要做的事情就是:

  • 將事件傳遞給最佳響應(yīng)者響應(yīng)

  • 事件沿著響應(yīng)鏈傳遞

事件傳遞給最佳響應(yīng)者

最佳響應(yīng)者具有最高的事件響應(yīng)優(yōu)先級并鸵,因此UIApplication會先將事件傳遞給它供其響應(yīng)。

UIApplication中有個(gè)sendEvent:的方法扔涧,在UIWindow中同樣也可以發(fā)現(xiàn)一個(gè)同樣的方法园担。UIApplication是通過這個(gè)方法把事件發(fā)送給UIWindow,然后UIWindow通過同樣的接口枯夜,把事件發(fā)送給最佳響應(yīng)者弯汰。

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

touchesBegan調(diào)用棧

當(dāng)事件傳遞給最佳響應(yīng)者后湖雹,響應(yīng)者響應(yīng)這個(gè)事件咏闪,則這個(gè)事件到此就結(jié)束了,它會被釋放摔吏。假設(shè)響應(yīng)者沒有響應(yīng)這個(gè)事件鸽嫂,那么它將何去何從?事件將會沿著響應(yīng)鏈自上而下傳遞征讲。

注意:
尋找最佳響應(yīng)者一節(jié)中也說到了事件的傳遞据某,與此處所說的事件的傳遞有本質(zhì)區(qū)別。上面所說的事件傳遞的目的是為了尋找事件的最佳響應(yīng)者诗箍,是自下而上(父視圖到子視圖)的傳遞癣籽;而這里的事件傳遞目的是響應(yīng)者做出對事件的響應(yīng),這個(gè)過程是自上而下(子視圖到父視圖)的扳还。前者為“尋找”才避,后者為“響應(yīng)”。

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

在UIKit中有一個(gè)類:UIResponder氨距,它是所有可以響應(yīng)事件的類的基類桑逝。來看下它的頭文件的幾個(gè)屬性和方法

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

--------------省略部分代碼------------
  
  // Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

UIApplication,UIViewController和UIView都是繼承自它俏让,都有一個(gè) nextResponder 方法楞遏,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個(gè)響應(yīng)者,也通過nextResponder來串成響應(yīng)鏈首昔。

在App中寡喝,所有的視圖都是根據(jù)樹狀層次結(jié)構(gòu)組織起來的,因此勒奇,每個(gè)View都有自己的SuperView预鬓。當(dāng)一個(gè)ViewaddSuperView上的時(shí)候,它的nextResponder屬性就會被指向它的SuperView赊颠,各個(gè)不同響應(yīng)者的指向如下:

  • UIView 若視圖是控制器的根視圖格二,則其nextResponder為控制器對象劈彪;否則,其nextResponder為父視圖顶猜。

  • UIViewController 若控制器的視圖是window的根視圖沧奴,則其nextResponder為窗口對象;若控制器是從別的控制器present出來的长窄,則其nextResponder為presenting view controller滔吠。

  • UIWindow nextResponder為UIApplication對象。

  • UIApplication 若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對象挠日,且不是UIView疮绷、UIViewController或app本身,則UIApplication的nextResponder為app delegate嚣潜。

這樣矗愧,整個(gè)App就通過nextResponder串成了一條鏈,也就是我們所說的響應(yīng)鏈郑原,子視圖指向父視圖構(gòu)成的響應(yīng)鏈唉韭。

看一下官網(wǎng)對于響應(yīng)鏈的示例展示

官網(wǎng)示例

若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:

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

圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的犯犁,則其nextResponderUIViewController對象属愤;若是直接addUIWindow上的,則其nextResponderUIWindow對象酸役。

響應(yīng)者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent: 方法控制的住诸,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞。

響應(yīng)者對于接收到的事件有3種操作:

  • 不攔截涣澡,默認(rèn)操作 事件會自動(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: 將事件往下傳遞

因此,你也可以通過 touchesBegan:withEvent:方法搞點(diǎn)事情~

總結(jié)

觸摸事件先通過自下而上(父視圖-->子視圖)的傳遞方式尋找最佳響應(yīng)者抗愁,
然后以自上而下(子視圖-->父視圖)的方式在響應(yīng)鏈中傳遞馁蒂。

Github :TouchEventDemo(僅供參考)

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜘腌,隨后出現(xiàn)的幾起案子沫屡,更是在濱河造成了極大的恐慌,老刑警劉巖撮珠,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沮脖,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)勺届,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門绷柒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涮因,你說我怎么就攤上這事∷耪溃” “怎么了养泡?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵漠其,是天一觀的道長巫俺。 經(jīng)常有香客問我梅猿,道長饥侵,這世上最難降的妖魔是什么缀皱? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任荧止,我火速辦了婚禮塘雳,結(jié)果婚禮上氏捞,老公的妹妹穿的比我還像新娘惩妇。我一直安慰自己株汉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布歌殃。 她就那樣靜靜地躺著乔妈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪氓皱。 梳的紋絲不亂的頭發(fā)上路召,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機(jī)與錄音波材,去河邊找鬼股淡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛廷区,可吹牛的內(nèi)容都是我干的唯灵。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼隙轻,長吁一口氣:“原來是場噩夢啊……” “哼早敬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起大脉,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤搞监,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后镰矿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琐驴,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绝淡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宙刘。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖牢酵,靈堂內(nèi)的尸體忽然破棺而出悬包,到底是詐尸還是另有隱情,我是刑警寧澤馍乙,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布布近,位于F島的核電站,受9級特大地震影響丝格,放射性物質(zhì)發(fā)生泄漏撑瞧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一显蝌、第九天 我趴在偏房一處隱蔽的房頂上張望预伺。 院中可真熱鬧,春花似錦曼尊、人聲如沸酬诀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽料滥。三九已至,卻和暖如春艾船,著一層夾襖步出監(jiān)牢的瞬間葵腹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工屿岂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留践宴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓爷怀,卻偏偏與公主長得像阻肩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子运授,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

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