事件傳遞及響應詳解

一.UIResponder

?? 1.UIResponder簡介

一個UIResponder類為那些需要響應并處理事件的對象定義了一組接口。

這些事件主要分為兩類:觸摸事件(touch events)和運動事件(motion events)。

UIResponder類為這兩類事件都定義了一組接口,這個我們將在下面詳細描述。

在UIKit中,UIApplication、UIView、UIViewController這幾個類都是直接繼承自UIResponder類。另外SpriteKit中的SKNode也是繼承自UIResponder類。因此UIKit中的視圖、控件、視圖控制器,以及我們自定義的視圖及視圖控制器都有響應事件的能力。這些對象通常被稱為響應對象,或者是響應者(以下我們統(tǒng)一使用響應者)。



?? 2.管理響應鏈

UIResponder提供了幾個方法來管理響應鏈,包括讓響應對象成為第一響應者、放棄第一響應者、檢測是否是第一響應者以及傳遞事件到下一響應者的方法,我們分別來介紹一下。

上面提到在響應鏈中負責傳遞事件的方法是nextResponder,其聲明如下:

- (UIResponder *)nextResponder

UIResponder類并不自動保存或設置下一個響應者,該方法的默認實現(xiàn)是返回nil。子類的實現(xiàn)必須重寫這個方法來設置下一響應者。UIView的實現(xiàn)是返回管理它的UIViewController對象(如果它有)或者其父視圖。而UIViewController的實現(xiàn)是返回它的視圖的父視圖;UIWindow的實現(xiàn)是返回UIApplication對象;而UIApplication的實現(xiàn)是返回nil。所以,響應鏈是在構(gòu)建視圖層次結(jié)構(gòu)時生成的。

事件順著響應傳遞順序:UIView控件 -> 父視圖 ->? UIViewController? ->? UIWindow - >? UIApplication -> nil

響應過程


一個響應對象可以成為第一響應者,也可以放棄第一響應者。為此,UIResponder提供了一系列方法,我們分別來介紹一下。

如果想判定一個響應對象是否是第一響應者,則可以使用以下方法:

- (BOOL)isFirstResponder

如果我們希望將一個響應對象作為第一響應者,則可以使用以下方法:

- (BOOL)becomeFirstResponder

如果對象成為第一響應者,則返回YES;否則返回NO。默認實現(xiàn)是返回YES。子類可以重寫這個方法來更新狀態(tài),或者來執(zhí)行一些其它的行為。

上面提到一個響應對象成為第一響應者的一個前提是它可以成為第一響應者,我們可以使用canBecomeFirstResponder方法來檢測,

- (BOOL)canBecomeFirstResponder

與上面兩個方法相對應的是響應者放棄第一響應者的方法,其定義如下:

- (BOOL)resignFirstResponder

- (BOOL)canResignFirstResponder

resignFirstResponder默認也是返回YES。需要注意的是,如果子類要重寫這個方法,則在我們的代碼中必須調(diào)用super的實現(xiàn)。

canResignFirstResponder默認也是返回YES。不過有些情況下可能需要返回NO,如一個輸入框在輸入過程中可能需要讓這個方法返回NO,以確保在編輯過程中能始終保證是第一響應者。


??????

????? 3.管理輸入視圖

???????? 所謂的輸入視圖,是指當對象為第一響應者時,顯示另外一個視圖用來處理當前對象的信息輸入,如UITextView和UITextField兩個對象,在其成為第一響應者是,會顯示一個系統(tǒng)鍵盤,用來輸入信息。這個系統(tǒng)鍵盤就是輸入視圖。輸入視圖有兩種,一個是inputView,另一個是inputAccessoryView。這兩者如圖3所示:

與inputView相關(guān)的屬性有如下兩個,

@property(nonatomic, readonly, retain) UIView *inputView

@property(nonatomic, readonly, retain) UIInputViewController *inputViewController

這兩個屬性提供一個視圖(或視圖控制器)用于替代為UITextField和UITextView彈出的系統(tǒng)鍵盤。我們可以在子類中將這兩個屬性重新定義為讀寫屬性來設置這個屬性。如果我們需要自己寫一個鍵盤的,如為輸入框定義一個用于輸入身份證的鍵盤(只包含0-9和X),則可以使用這兩個屬性來獲取這個鍵盤。

與inputView類似,inputAccessoryView也有兩個相關(guān)的屬性:

@property(nonatomic, readonly, retain) UIView *inputAccessoryView

@property(nonatomic, readonly, retain) UIInputViewController *inputAccessoryViewController

設置方法與前面相同,都是在子類中重新定義為可讀寫屬性,以設置這個屬性。

另外,UIResponder還提供了以下方法,在對象是第一響應者時更新輸入和訪問視圖,

- (void)reloadInputViews

調(diào)用這個方法時,視圖會立即被替換,即不會有動畫之類的過渡。如果當前對象不是第一響應者,則該方法是無效的。



?????? 4.驗證命令

在我們的應用中,經(jīng)常會處理各種菜單命令,如文本輸入框的”復制”、”粘貼”等。UIResponder為此提供了兩個方法來支持此類操作。首先使用以下方法可以啟動或禁用指定的命令:

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender

該方法默認返回YES,我們的類可以通過某種途徑處理這個命令,包括類本身或者其下一個響應者。子類可以重寫這個方法來開啟菜單命令。例如,如果我們希望菜單支持”Copy”而不支持”Paser”,則在我們的子類中實現(xiàn)該方法。需要注意的是,即使在子類中禁用某個命令,在響應鏈上的其它響應者也可能會處理這些命令。

另外,我們可以使用以下方法來獲取可以響應某一行為的接收者:

- (id)targetForAction:(SEL)action withSender:(id)sender

在對象需要調(diào)用一個action操作時調(diào)用該方法。默認的實現(xiàn)是調(diào)用canPerformAction:withSender:方法來確定對象是否可以調(diào)用action操作。如果可以,則返回對象本身,否則將請求傳遞到響應鏈上。如果我們想要重寫目標的選擇方式,則應該重寫這個方法。下面這段代碼演示了一個文本輸入域禁用拷貝/粘貼操作:

- (id)targetForAction:(SEL)action withSender:(id)sender{? ?

?????????????? UIMenuController *menuController = [UIMenuController sharedMenuController];

????????? ? ? ? if (action == @selector(selectAll:) || action == @selector(paste:) ||action == @selector(copy:) || action==@selector(cut:)){

????????????????????? if (menuController){

???????????? ? ? ? ? ? ? ? ? ? [UIMenuController sharedMenuController].menuVisible = NO;

???????????????????? }

?????????????????? return nil;

?????????????? }

??????????? return [super targetForAction:action withSender:sender];

}



二.iOS中的事件的產(chǎn)生和傳遞過程


1.事件的產(chǎn)生

??????? 發(fā)生觸摸事件(以觸摸事件為例)后,系統(tǒng)會將該事件加入到一個由UIApplication管理的事件隊列中為什么是隊列而不是棧?因為隊列的特定是先進先出,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊列。

UIApplication會從事件隊列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常,先發(fā)送事件給應用程序的主窗口(keyWindow)。

主窗口會在視圖層次結(jié)構(gòu)中找到一個最合適的視圖(什么樣的視圖才算最合適的視圖???)來處理觸摸事件,這也是整個事件處理過程的第一步。

找到合適的視圖控件后,就會調(diào)用視圖控件的touches方法來作具體的事件處理。


2.事件的傳遞

事件傳遞過程: 觸摸事件的傳遞是從父控件傳遞到子控件,也就是: 產(chǎn)生觸摸事件->UIApplication事件隊列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view

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


前面一直在說最合適的視圖來處理觸摸事件,那么什么樣的視圖才算最合適的視圖???按照如下步驟找到的就是最合適的視圖:

1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件(系統(tǒng)底層如何談判?),如果能,那么在判斷觸摸點在不在窗口自己身上;

2.判斷觸摸點是否在自己身上(系統(tǒng)底層如何判斷?),在自己身上繼續(xù)第三步;

3.子控件數(shù)組中從后往前遍歷子控件,重復前面的兩個步驟(所謂從后往前遍歷子控件,就是首先查找子控件數(shù)組中最后一個元素,然后執(zhí)行1、2步驟);

4.view,比如叫做fitView,那么會把這個事件交給這個fitView,再遍歷這個fitView的子控件,直至沒有更合適的view為止。

5.如果沒有符合條件的子控件,那么就認為自己最合適處理這個事件,也就是自己是最合適的view

UIView不能接收觸摸事件的三種情況:

?????? 1.不允許交互:userInteractionEnabled = NO

?????? 2.隱藏:如果把父控件隱藏,那么子控件也會隱藏,隱藏的控件不能接受事件

?????? 3.透明度:如果設置一個控件的透明度<0.01,會直接影響子控件的透明度。alpha:0.0~0.01為透明。

?????? 注 意:默認UIImageView不能接受觸摸事件,因為不允許交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES。


2.1 尋找最合適的view底層剖析

UIView基類提供的2個方法

1. - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;? // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

只要事件一傳遞給一個控件,這個控件就會調(diào)用他自己的hitTest:withEvent:方法,方法可以返回最合適的view

注 意:不管這個控件能不能處理事件,也不管觸摸點在不在這個控件上,事件都會先傳遞給這個控件,隨后再調(diào)用hitTest:withEvent:方法; 如果hitTest:withEvent:方法中返回nil,那么調(diào)用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒有找到更合適的view。那么最合適的view就是該控件的父控件。


2. - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;? // default returns YES if point is in bounds

作用:判斷下傳入過來的點在不在方法調(diào)用者的坐標系上,代表點在方法調(diào)用者的坐標系上;返回NO代表點不在方法調(diào)用者的坐標系上,那么方法調(diào)用者也就不能處理事件。

尋找最合適的view底層剖析之hitTest:withEvent:方法底層做法


3.事件的響應

1>用戶點擊屏幕后產(chǎn)生的一個觸摸事件,經(jīng)過一系列的傳遞過程后,會找到最合適的視圖控件來處理這個事件;

2>找到最合適的視圖控件后,就會調(diào)用控件的touches方法來作具體的事件處理touchesBegan…touchesMoved…touchedEnded…3>這些touches方法的默認做法是將事件順著響應者鏈條向上傳遞(也就是touch方法默認不處理事件,只傳遞事件),將事件交給上一個響應者進行處理。

touches默認做法。


???????? 注:這個圖是從其他地方截取的,我個人認為有不合理的地方,[super touchesBegan:touches withEvent:event]這段代碼應該改為[[self nextResponder] touchesBegan:touches withEvent:event] 或者 [[self superview] touchesBegan:touches withEvent:event];因為響應鏈是往上找 下一個響應者 或者 父控件 而不是去父類里面找touch方法,然后調(diào)用。帶著這個疑惑我自己寫了個代碼發(fā)現(xiàn)按這兩種方式寫都沒有問題。為何會這樣呢???

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

1.觸摸屏幕產(chǎn)生觸摸事件后,觸摸事件會被添加到由UIApplication管理的事件隊列中(即,首先接收到事件的是UIApplication)。

2.UIApplication會從事件隊列中取出最前面的事件,把事件傳遞給應用程序的主窗口(keyWindow)。

3.主窗口會在視圖層次結(jié)構(gòu)中找到一個最合適的視圖來處理觸摸事件。(至此,事件傳遞已完成)

4.最合適的view會調(diào)用自己的touches方法處理事件

5.touches默認做法是把事件順著響應者鏈條向上拋。


3.1 4 實際項目中的應用

情景1: 點擊子控件,讓父控件響應事件;

實現(xiàn)方式一 : 因為hitTest:withEvent:方法的作用就是控件接收到事件后,判斷自己是否能處理事件,判斷點在不在自己的坐標系上,然后返回最合適的view。所以,我們可以在hitTest:withEvent:方法里面強制返回父控件為最合適的view

#import "GreenView2.h"

@implementation GreenView2

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

????????? return [self superview]; // return nil;

????????? // 此處返回nil也可以。返回nil就相當于當前的view不是最合適的view

}

@end


實現(xiàn)方式二: 讓誰響應,就直接重寫誰的touchesBegan: withEvent:方法

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

??? ? ? ? ?? NSLog(@"do somthing...");

}



情景2:點擊子控件,父控件和子控件都響應事件

實現(xiàn)方式事件的響應是順著響應者鏈條向上傳遞的,即從子控件傳遞給父控件,touch方法默認不處理事件,而是把事件順著響應者鏈條傳遞給上一個響應者。這樣我們就可以依托這個原理,讓一個事件多個控件響應
#import "GreenView2.h"

@implementation GreenView2

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

?????????? NSLog(@"-- touchGreen");

? ? ? ? if ([self nextResponder]) {

???????????? [[self nextResponder] touchesBegan:touches withEvent:event];

?????? }

??????? //或者使用父控件

? ? ? ? // [[self superview] touchesBegan:touches withEvent:event];

}



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

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

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