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

APP通過響應(yīng)者對象來接收和處理事件。響應(yīng)者對象:只有繼承了UIResponder或者UIView, UIViewController, UIApplication的實例對象,才有響應(yīng)和處理事件的能力,才能被稱為響應(yīng)者對象。

因為UIView, UIViewController, UIApplication繼承了UIResponder。

事件傳遞

響應(yīng)者接收到原始事件,一定會要么處理掉這個事件,要么向下一個響應(yīng)者對象傳遞。

當(dāng)APP接收到一個事件時,UIKit會自動引導(dǎo)這個事件到最合適的響應(yīng)者對象,也就是它的第一響應(yīng)者。沒有處理的事件,會在活躍的響應(yīng)鏈中,從一個響應(yīng)者傳遞到另一個響應(yīng)者。這個活躍的響應(yīng)鏈?zhǔn)茿PP的響應(yīng)對象的動態(tài)組成。對于對象怎么從一個響應(yīng)者傳遞到下一個響應(yīng)者,UIKit定義了默認(rèn)的規(guī)則,但是我們也可以通過復(fù)寫APP中的響應(yīng)者的屬性來改變這些規(guī)則。

判斷事件的第一響應(yīng)者:

對于每種類型的時間,UIKit會指派第一響應(yīng)者并首先將事件發(fā)送給這個響應(yīng)者對象。基于不同的事件類型,第一響應(yīng)者也是不同的:

觸摸事件:第一響應(yīng)者是觸摸產(chǎn)生的view;

按壓事件:第一響應(yīng)者是焦點所在的響應(yīng)者;

搖動事件:第一響應(yīng)者是UIKit判決為第一響應(yīng)者的對象;

遠(yuǎn)程控制事件:第一響應(yīng)者是UIKit判決為第一響應(yīng)者的對象;

編輯菜單消息:第一響應(yīng)者是UIKit判決為第一響應(yīng)者的對象;

控件使用action消息直接與與其關(guān)聯(lián)的目標(biāo)對象通信。當(dāng)用戶和一個控件交互的時候,這個控件就會調(diào)用它的目標(biāo)對象的action方法,換句話說,它給它的目標(biāo)對象發(fā)送了一個action消息。

action消息不是事件,但是它也會利用響應(yīng)鏈。當(dāng)控件的目標(biāo)對象是nil時,UIKit從目標(biāo)對象開始,沿著響應(yīng)鏈遍歷,直到找到實現(xiàn)了合適action方法的對象。舉個例子,UIKit編輯菜單使用這個方法去搜索實現(xiàn)了cut: , copy:, paste: 等這樣的方法的響應(yīng)對象。

如果view有附加的手勢識別器gesture recognizer,那么手勢識別器會在view接收觸摸、按壓事件之前接收這些事件。如果所有的view的手勢識別器都沒有識別手勢,那么,事件會被傳遞給view處理。如果view也沒有處理的話,UIKit就會把事件在響應(yīng)鏈上傳遞。

判斷哪個響應(yīng)者包含了觸摸事件

當(dāng)觸摸事件發(fā)生時,UIKit會把這個事件自動添加到UIApplication管理事件的隊列中。

處理的時候,先從UIApplication的事件隊列中取出最前面的事件,進行事件的分發(fā)傳遞。傳遞的順序是從父控件自上而下傳遞到子控件。UIKit使用基于view的hit-testing來把觸摸的位置和視圖層級中的view對象的邊界作比較,來判決觸摸事件發(fā)生在哪里。

UIView的hitTest:withEvent: 方法遍歷視圖層級,從最遠(yuǎn)的后代,也就是子view數(shù)組的后面開始尋找包含這個觸摸的子view。那么這個view就是這個觸摸事件的第一響應(yīng)者。找到合適的子view的時候,就調(diào)用這個子控件的觸摸方法來進行具體的處理。之所以在遍歷子控件的時候,要從后往前遍歷,是因為后添加的view一般是在靠上面的位置,它接受事件的可能性也比較大,因此從后往前遍歷。

不能響應(yīng)的控件:1、該控件交互開關(guān)userInteractionEnabled為No;2、該控件處于隱藏Hidden狀態(tài);3、該控件的透明度小于0.01。

事件傳遞剖析

//這個方法會遞歸調(diào)用-pointInside:withEvent:,返回包含point的view

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

//如果點在邊界范圍內(nèi),就默認(rèn)返回YES

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;?

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

參數(shù):(CGPoint)point接收方的坐標(biāo)系(邊界)中指定的點。

? ? ? ? ? (nullable UIEvent *)event對這個方法調(diào)用的事件。 如果您從事件處理代碼之外調(diào)用此方法,則可以指定nil。

返回值:返回的視圖對象是當(dāng)前視圖的最遠(yuǎn)子視圖并且這個子視圖包含了這個點。 如果點完全位于接收方的視圖層次之外,則返回nil。

這個方法通過對每一個子view調(diào)用-pointInside:withEvent:這個方法遍歷整個視圖層級體系,來判斷哪個子view應(yīng)該接收觸摸事件。如果一個子view的-pointInside:withEvent:返回了YES,那么這個子view的整個視圖層級就會被遍歷,直到在其子view中找到包含這個特定點的最靠上的view。如果view中沒有包含這個點,那么它的視圖體系就會被忽略而不去遍歷。一般我們不需要自己去調(diào)用這個方法,除非是為了隱藏或改變來自其子view的觸摸事件才會去復(fù)寫這個方法。

這個方法對于那些被隱藏,被禁用用戶交互開關(guān),或者透明度小于0.01的不能響應(yīng)的控件,是不會被調(diào)用的。

注意:

如果觸摸位置在view的邊界之外的話,hitTest:withEvent:方法會忽略這個view及其子view。因此,當(dāng)一個view的clipsToBounds 屬性為NO時,這個view邊界之外的子view不會被返回,即使它包含了這個觸摸。之前在做群聊答疑的入口時遇到過這個問題,也是通過復(fù)寫hitTest方法來解決的

當(dāng)事件傳遞給某一個控件時,就會調(diào)用這個控件的- (nullable UIView?)hitTest:(CGPoint)point withEvent:(nullable UIEvent?)event; 方法。

下面是總結(jié)的事件傳遞流程:

事件傳遞流程

事件響應(yīng)

UIResponder用來處理事件的響應(yīng)方法有:

觸摸事件的四個處理方法:

//開始觸摸時,自動調(diào)用該方法

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;

//觸摸滑動時,自動調(diào)用該方法

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

//觸摸結(jié)束離開時,自動調(diào)用該方法

- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;

//觸摸結(jié)束前,某些意外事件,如來電話,中止觸摸操作時,自動調(diào)用該方法

- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

按壓事件的四個處理方法:

- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

在這里,我們只討論觸摸事件的響應(yīng)方法

觸摸事件中,如果是兩只手指同時開始觸摸,那么,- (void)touchesBegan:(NSSet?)touches withEvent:(nullable UIEvent?)event;的touches參數(shù)Set集合中,就會有兩個UITouch對象。

如果想要自定義UIView的觸摸事件,那么就需要在自定義的UIView子類中,重寫這四個方法。而如果是UIViewController想自定義觸摸事件的話,直接復(fù)寫上面四個方法就可以了,因為UIViewController本身就自帶self.view;

事件的響應(yīng)是自下而上響應(yīng)的。也就是說,當(dāng)響應(yīng)者view沒有處理觸摸事件時,可以沿著響應(yīng)者鏈向上調(diào)用其父view的觸摸方法;在根view中,響應(yīng)鏈在傳遞到窗口之前,會先傳遞到視圖控制器。如果窗口window沒有處理這個事件的話,UIKit會把事件傳遞到UIApplication對象。

我們可以通過復(fù)寫響應(yīng)者對象的nextResponder屬性來改變響應(yīng)者鏈。這么做了以后,下一個的響應(yīng)者就是我們要返回的對象。

許多UIKit類可以復(fù)寫這個屬性,返回特定的對象。如

UIView對象。如果視圖是視圖控制器的根視圖,那么下一個的響應(yīng)者就是視圖控制器。否則,就是視圖的父視圖。

UIViewController對象。如果視圖控制器的視圖是窗口的根視圖,那么下一個的響應(yīng)者是窗口對象。如果視圖控制器是被其他的視圖控制器呈現(xiàn)出來,那么下一個的響應(yīng)者,就是正在呈現(xiàn)的視圖控制器。

UIWindow對象。窗口的下一個響應(yīng)者就是UIApplication對象。

UIApplication對象。下一個響應(yīng)者是APP Delegate,但前提是APP Delegate是UIResponder的實例,并且不是視圖,視圖控制器或者APP對象本身。

下面我簡單的寫了demo來,驗證事件的傳遞和響應(yīng)過程:

所建立的視圖層級關(guān)系如下:

視圖層級關(guān)系

自定義的view類如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

{

? ? NSLog(@"ViewA touchesBegan with %lu finger", (unsigned long)touches.count);

}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

{

? ? NSLog(@"ViewA touchesMoved with %lu finger", (unsigned long)touches.count);

}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

{

? ? NSLog(@"ViewA touchesEnded with %lu finger", (unsigned long)touches.count);

}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

{

? ? NSLog(@"ViewA touchesCancelled with %lu finger", (unsigned long)touches.count);

}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

? ? NSLog(@"傳遞到ViewA");

? ? UIView *view = [super hitTest:point withEvent:event];

? ? NSLog(@"傳遞到ViewA, view的name = %@", [view class]);

? ? return view;

}

當(dāng)點擊viewD的時候,事件的傳遞過程可以通過 hitTest:withEvent:方法里打印的日志看出,這個觸摸事件是沿著ViewA->ViewC->viewE->viewD傳遞的,而事件的響應(yīng)由于最終的響應(yīng)者viewD在它的觸摸方法中做了處理,因此,事件的響應(yīng)只有viewD。

而如果viewC,viewD都不想去處理這個觸摸事件,那么我們可以把viewC和viewD中的觸摸方法注釋掉,這樣viewC和viewD就都不會處理這個事件了。那么這個觸摸事件就會向上傳遞給viewA來處理。日志如下:

總結(jié)

以上就是觸摸事件的傳遞和響應(yīng)過程,總而言之,事件的傳遞是自上而下的,事件的響應(yīng)是自下而上的。具體要怎么傳遞,怎么響應(yīng),可以通過去重寫hit-test方法和touch方法來實現(xiàn)。

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

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

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