iOS 響應(yīng)鏈

當(dāng)我們在使用微信等工具恶座,點(diǎn)擊掃一掃,就能打開二維碼掃描視圖睡蟋。在我們點(diǎn)擊屏幕的時(shí)候款票,iphone OS獲取到了用戶進(jìn)行了“單擊”這一行為控硼,操作系統(tǒng)把包含這些點(diǎn)擊事件的信息包裝成UITouch和UIEvent形式的實(shí)例,然后找到當(dāng)前運(yùn)行的程序艾少,逐級尋找能夠響應(yīng)這個(gè)事件的對象卡乾,直到?jīng)]有響應(yīng)者響應(yīng)。這一尋找的過程缚够,被稱作事件的響應(yīng)鏈幔妨,如下圖所示,不用的響應(yīng)者以鏈?zhǔn)降姆绞綄ふ?br>

事件響應(yīng)鏈

響應(yīng)者

在iOS中潮瓶,能夠響應(yīng)事件的對象都是UIResponder的子類對象陶冷。UIResponder提供了四個(gè)用戶點(diǎn)擊的回調(diào)方法,分別對應(yīng)用戶點(diǎn)擊開始毯辅、移動(dòng)、點(diǎn)擊結(jié)束以及取消點(diǎn)擊煞额,其中只有在程序強(qiáng)制退出或者來電時(shí)思恐,取消點(diǎn)擊事件才會(huì)調(diào)用沾谜。

UIResponder的點(diǎn)擊事件

在自定義UIView為基類的控件時(shí),我們可以重寫這幾個(gè)方法來進(jìn)行點(diǎn)擊回調(diào)胀莹。在回調(diào)中基跑,我們可以看到方法接收兩個(gè)參數(shù),一個(gè)UITouch對象的集合描焰,還有一個(gè)UIEvent對象媳否。這兩個(gè)參數(shù)分別代表的是點(diǎn)擊對象和事件對象。

事件對象

iOS使用UIEvent表示用戶交互的事件對象荆秦,在UIEvent.h文件中篱竭,我們可以看到有一個(gè)UIEventType類型的屬性,這個(gè)屬性表示了當(dāng)前的響應(yīng)事件類型步绸。分別有多點(diǎn)觸控掺逼、搖一搖以及遠(yuǎn)程操作(在iOS之后新增了3DTouch事件類型)。在一個(gè)用戶點(diǎn)擊事件處理過程中瓤介,UIEvent對象是唯一的

點(diǎn)擊對象

UITouch表示單個(gè)點(diǎn)擊吕喘,其類文件中存在枚舉類型UITouchPhase的屬性,用來表示當(dāng)前點(diǎn)擊的狀態(tài)刑桑。這些狀態(tài)包括點(diǎn)擊開始氯质、移動(dòng)、停止不動(dòng)祠斧、結(jié)束和取消五個(gè)狀態(tài)病梢。每次點(diǎn)擊發(fā)生的時(shí)候,點(diǎn)擊對象都放在一個(gè)集合中傳入U(xiǎn)IResponder的回調(diào)方法中梁肿,我們通過集合中對象獲取用戶點(diǎn)擊的位置蜓陌。其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當(dāng)前點(diǎn)擊坐標(biāo)點(diǎn),- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個(gè)點(diǎn)擊位置的坐標(biāo)點(diǎn)吩蔑。

為了確認(rèn)UIView確實(shí)是通過UIResponder的點(diǎn)擊方法響應(yīng)點(diǎn)擊事件的钮热,我創(chuàng)建了UIView的類別,并重寫+ (void)load方法烛芬,使用method_swizzling的方式交換點(diǎn)擊事件的實(shí)現(xiàn)

+?(void)load

Method?origin?=?class_getInstanceMethod([UIView?class],?@selector(touchesBegan:withEvent:));

Method?custom?=?class_getInstanceMethod([UIView?class],?@selector(lxd_touchesBegan:withEvent:));

method_exchangeImplementations(origin,?custom);

origin?=?class_getInstanceMethod([UIView?class],?@selector(touchesMoved:withEvent:));

custom?=?class_getInstanceMethod([UIView?class],?@selector(lxd_touchesMoved:withEvent:));

method_exchangeImplementations(origin,?custom);

origin?=?class_getInstanceMethod([UIView?class],?@selector(touchesEnded:withEvent:));

custom?=?class_getInstanceMethod([UIView?class],?@selector(lxd_touchesEnded:withEvent:));

method_exchangeImplementations(origin,?custom);

}

-?(void)lxd_touchesBegan:?(NSSet?*)touches?withEvent:?(UIEvent?*)event

{

NSLog(@"%@?---?begin",?self.class);

[self?lxd_touchesBegan:?touches?withEvent:?event];

}

-?(void)lxd_touchesMoved:?(NSSet?*)touches?withEvent:?(UIEvent?*)event

{

NSLog(@"%@?---?move",?self.class);

[self?lxd_touchesMoved:?touches?withEvent:?event];

}

-?(void)lxd_touchesEnded:?(NSSet?*)touches?withEvent:?(UIEvent?*)event

{

NSLog(@"%@?---?end",?self.class);

[self?lxd_touchesEnded:?touches?withEvent:?event];

}

在新建的項(xiàng)目中隧期,我分別創(chuàng)建了AView、BView赘娄、CView和DView四個(gè)UIView的子類仆潮,然后點(diǎn)擊任意一個(gè)位置:

項(xiàng)目結(jié)構(gòu)圖

在我點(diǎn)擊上圖綠色視圖的時(shí)候,控制臺(tái)輸出了下面的日志(日期部分已經(jīng)去除):

CView?---?begin

CView?---?end

由此可見在我們點(diǎn)擊UIView的時(shí)候遣臼,是通過touches相關(guān)的點(diǎn)擊事件進(jìn)行回調(diào)處理的性置。

除了touches回調(diào)的幾個(gè)點(diǎn)擊事件,手勢UIGestureRecognizer對象也可以附加在view上揍堰,來實(shí)現(xiàn)其他豐富的手勢事件鹏浅。在view添加單擊手勢之后嗅义,原來的touchesEnded方法就無效了。最開始我一直認(rèn)為view添加手勢之后隐砸,原有的touches系列方法全部無效之碗。但是在測試demo中,發(fā)現(xiàn)view添加手勢之后季希,touchesBegan方法是有進(jìn)行回調(diào)的褪那,但是moved跟ended就沒有進(jìn)行回調(diào)。因此式塌,在系統(tǒng)的touches事件處理中博敬,在touchesBegan之后,應(yīng)該是存在著一個(gè)調(diào)度后續(xù)事件(nextHandler)處理的方法珊搀,個(gè)人猜測事件調(diào)度的處理大致如下圖示:

事件調(diào)度

響應(yīng)鏈傳遞

上面已經(jīng)介紹了某個(gè)控件在接收到點(diǎn)擊事件時(shí)的處理冶忱,那么系統(tǒng)是怎么通過用戶點(diǎn)擊的位置找到處理點(diǎn)擊事件的view的呢?

在上文我們已經(jīng)說過了系統(tǒng)通過不斷查找下一個(gè)響應(yīng)者來響應(yīng)點(diǎn)擊事件境析,而所有的可交互控件都是UIResponder直接或者間接的子類囚枪,那么我們是否可以在這個(gè)類的頭文件中找到關(guān)鍵的屬性呢?

正好存在著這么一個(gè)方法:- (nullable UIResponder *)nextResponder劳淆,通過方法名我們不難發(fā)現(xiàn)這是獲取當(dāng)前view的下一個(gè)響應(yīng)者链沼,那么我們重寫touchesBegan方法,逐級獲取下一響應(yīng)者沛鸵,直到?jīng)]有下一個(gè)響應(yīng)者位置括勺。相關(guān)代碼如下:

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

{

UIResponder?*?next?=?[self?nextResponder];

NSMutableString?*?prefix?=?@"".mutableCopy;

while?(next?!=?nil)?{

NSLog(@"%@%@",?prefix,?[next?class]);

[prefix?appendString:?@"--"];

next?=?[next?nextResponder];

}

}

控制臺(tái)輸出的所有下級事件響應(yīng)者如下:

AView

--UIView

----ViewController

------UIWindow

--------UIApplication

----------AppDelegate

雖然結(jié)果非常有層次,但是從系統(tǒng)逐級查找響應(yīng)者的角度上來說曲掰,這個(gè)輸出的順序是剛好相反的疾捍。為什么會(huì)出現(xiàn)這種問題呢?我們可以看到輸出中存在一個(gè)ViewController類栏妖,說明UIViewController也是UIResponder的子類乱豆。但是我們可以發(fā)現(xiàn),controller是一個(gè)view的管理者吊趾,即便它是響應(yīng)鏈的成員之一宛裕,但是按照邏輯來說,控制器不應(yīng)該是系統(tǒng)查找對象之一论泛,通過nextResponder方法查找的這個(gè)思路是不正確的揩尸。

后來,發(fā)現(xiàn)在UIView的頭文件中存在這么兩個(gè)方法屁奏,分別返回UIView和BOOL類型的方法:

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

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

根據(jù)方法名岩榆,一個(gè)是根據(jù)點(diǎn)擊坐標(biāo)返回事件是否發(fā)生在本視圖以內(nèi),另一個(gè)方法是返回響應(yīng)點(diǎn)擊事件的對象。通過這兩個(gè)方法朗恳,我們可以猜到湿颅,系統(tǒng)在收到點(diǎn)擊事件的時(shí)候通過不斷遍歷當(dāng)前視圖上的子視圖的這些方法载绿,獲取下一個(gè)響應(yīng)的視圖粥诫。因此,繼續(xù)通過method_swizzling方式修改這兩個(gè)方法的實(shí)現(xiàn)崭庸,并且測試輸出如下:

UIStatusBarWindow?can?answer?1

UIStatusBar?can?answer?0

UIStatusBarForegroundView?can?answer?0

UIStatusBarServiceItemView?can?answer?0

UIStatusBarDataNetworkItemView?can?answer?0

UIStatusBarBatteryItemView?can?answer?0

UIStatusBarTimeItemView?can?answer?0

hit?view:?UIStatusBar

hit?view:?UIStatusBarWindow

UIWindow?can?answer?1

UIView?can?answer?1

hit?view:?_UILayoutGuide

hit?view:?_UILayoutGuide

AView?can?answer?1

DView?can?answer?0

hit?view:?DView

BView?can?answer?0

hit?view:?BView

hit?view:?AView

hit?view:?UIView

hit?view:?UIWindow

......??//下面是touches方法的輸出

最上面的UIStatusBar開頭的類型大家可能沒見過怀浆,但是不妨礙我們猜到這是狀態(tài)欄相關(guān)的一些視圖,具體可以查找蘋果的文檔中心(Xcode中快捷鍵shift+command+0打開)怕享。從輸出中不難看出系統(tǒng)先調(diào)用pointInSide: WithEvent:判斷當(dāng)前視圖以及這些視圖的子視圖是否能接收這次點(diǎn)擊事件执赡,然后在調(diào)用hitTest: withEvent:依次獲取處理這個(gè)事件的所有視圖對象,在獲取所有的可處理事件對象后函筋,開始調(diào)用這些對象的touches回調(diào)方法

通過輸出的方法調(diào)用沙合,我們可以看到響應(yīng)查找的順序是:UIStatusBar相關(guān)的視圖 ->UIWindow->UIView->AView->DView->BView(系統(tǒng)在事件鏈傳遞的過程中一定會(huì)遍歷所有的子視圖判斷是否能夠響應(yīng)點(diǎn)擊事件),以本文demo為例跌帐,我們可以得出事件響應(yīng)鏈查找的圖示如下:

響應(yīng)者查找流程

那么在上面的查找響應(yīng)者流程完成之后首懈,系統(tǒng)會(huì)將本次事件中的點(diǎn)擊轉(zhuǎn)換成UITouch對象,然后將這些對象和UIEvent類型的事件對象傳遞給touchesBegan方法谨敛,you

不僅如此究履,從上面輸出的nextResponder來看,所有的響應(yīng)者都是在查找中返回可響應(yīng)點(diǎn)擊的視圖脸狸。因此最仑,我們可以推測出UIApplication對象維護(hù)著自己的一個(gè)響應(yīng)者棧,當(dāng)pointInSide: withEvent:返回yes的時(shí)候炊甲,響應(yīng)者入棧泥彤。

響應(yīng)者棧

棧頂?shù)捻憫?yīng)者作為最優(yōu)先處理事件的對象,假設(shè)AView不處理事件卿啡,那么出棧绵疲,移交給UIView,以此下去粪牲,直到事件得到了處理或者到達(dá)AppDelegate后依舊未響應(yīng)欣舵,事件被摒棄為止。通過這個(gè)機(jī)制我們也可以看到controller是響應(yīng)者棧中的例外揭鳞,即便沒有pointInSide: withEvent:的方法返回可響應(yīng)炕贵,controller依舊能夠入棧成為UIView的下一個(gè)響應(yīng)者。

響應(yīng)鏈應(yīng)用

既然已經(jīng)知道了系統(tǒng)是怎么獲取響應(yīng)視圖的流程了野崇,那么我們可以通過重寫查找事件處理者的方法來實(shí)現(xiàn)不規(guī)則形狀點(diǎn)擊称开。最常見的不規(guī)則視圖就是圓形視圖,在demo中我設(shè)置view的寬高為200,那么重寫方法事件如下:

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

{

const?CGFloat?halfWidth?=?100;

CGFloat?xOffset?=?point.x?-?100;

CGFloat?yOffset?=?point.y?-?100;

CGFloat?radius?=?sqrt(xOffset?*?xOffset?+?yOffset?*?yOffset);

return?radius?<=?halfWidth;

}

最終的效果圖如下:

不規(guī)則形狀點(diǎn)擊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鳖轰,一起剝皮案震驚了整個(gè)濱河市清酥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蕴侣,老刑警劉巖焰轻,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異昆雀,居然都是意外死亡辱志,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門狞膘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揩懒,“玉大人,你說我怎么就攤上這事挽封∫亚颍” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵辅愿,是天一觀的道長智亮。 經(jīng)常有香客問我,道長渠缕,這世上最難降的妖魔是什么鸽素? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮亦鳞,結(jié)果婚禮上馍忽,老公的妹妹穿的比我還像新娘。我一直安慰自己燕差,他們只是感情好遭笋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著徒探,像睡著了一般瓦呼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上测暗,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天央串,我揣著相機(jī)與錄音,去河邊找鬼碗啄。 笑死质和,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的稚字。 我是一名探鬼主播饲宿,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼厦酬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瘫想?” 一聲冷哼從身側(cè)響起仗阅,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎国夜,沒想到半個(gè)月后减噪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡支竹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年旋廷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸠按。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片礼搁。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖目尖,靈堂內(nèi)的尸體忽然破棺而出馒吴,到底是詐尸還是另有隱情,我是刑警寧澤瑟曲,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布饮戳,位于F島的核電站,受9級特大地震影響洞拨,放射性物質(zhì)發(fā)生泄漏扯罐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一烦衣、第九天 我趴在偏房一處隱蔽的房頂上張望歹河。 院中可真熱鬧,春花似錦花吟、人聲如沸秸歧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽键菱。三九已至,卻和暖如春今布,著一層夾襖步出監(jiān)牢的瞬間经备,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工部默, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侵蒙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓甩牺,卻偏偏與公主長得像蘑志,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

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

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的急但?困惑于Cell怎么突然不能點(diǎn)擊了澎媒?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 57,082評論 51 599
  • 最近在寫一個(gè)圖片瀏覽的需求波桩,一些地方我使用了響應(yīng)者來處理戒努,順便又去看看了官方文檔,這里記錄一下官方文檔镐躲,并給出一些...
    HelloAda閱讀 10,867評論 3 36
  • 在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件储玫。本想自己總結(jié)一下,但是遇到了這篇文章萤皂,感覺總結(jié)的已經(jīng)很到位撒穷,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,010評論 4 26
  • 本文來自:http://ios.jobbole.com/84081/ 前言: 按照時(shí)間順序裆熙,事件的生命周期是這樣的...
    HackerOnce閱讀 2,836評論 1 10
  • 首先清楚兩個(gè)概念響應(yīng)者:對用戶交互動(dòng)作事件進(jìn)行響應(yīng)的對象端礼。響應(yīng)者鏈:成為處理事件的響應(yīng)者的先后順序鏈。平時(shí)當(dāng)我們點(diǎn)...
    mengyingguo閱讀 387評論 0 1