iOS的事件有好幾種:Touch Events(觸摸事件)、Motion Events(運動事件耍目,比如重力感應和搖一搖等)觉鼻、Remote Events(遠程事件朋鞍,比如用耳機上得按鍵來控制手機)惯雳,其中最常用的應該就是Touch Events了朝巫,基本存在于每個app的每個地方,今天我們主要就講講它石景,至于其他兩個事件有興趣的可以自行查閱資料劈猿。
在網(wǎng)頁上當我們講到事件,我們會講到事件響應鏈潮孽,我們會講到事件的響應者和事件的傳遞方式(冒泡)揪荣,那么在app上,其實也離不開這幾個問題往史,今天我們也重這幾個方面來介紹iOS的事件機制:?
1仗颈、響應鏈是什么時候怎樣構(gòu)建的??
2椎例、事件第一個響應者是怎么確定的挨决?
?3、事件第一個響應者確定后粟矿,系統(tǒng)是怎樣傳遞事件的?
##響應鏈的構(gòu)建
無論是哪種事件损拢,其傳遞和響應都與響應鏈息息相關(guān)陌粹,那么響應鏈到底是一個什么樣的東西呢? 在UIKit中有一個類:UIResponder福压,我們可以看看頭文件的幾個屬性和方法:
UIResponder是所有可以響應事件的類的基類(從名字應該就可以看出來了)掏秩,其中包括最常見的UIView和UIViewController甚至是UIApplication或舞,所以我們的UIView和UIViewController都是作為響應事件的載體。
那么響應鏈跟這個UIResponder有什么關(guān)系呢蒙幻?事實事件響應鏈的形成和事件的響應和傳遞映凳,UIResponder都幫我們做了很多事。我們的app中邮破,所有的視圖都是按照一定的結(jié)構(gòu)組織起來的诈豌,即樹狀層次結(jié)構(gòu),每個view都有自己的superView抒和,包括controller的topmost view(controller的self.view)矫渔。當一個view被add到superView上的時候,他的nextResponder屬性就會被指向它的superView摧莽,當controller被初始化的時候庙洼,self.view(topmost view)的nextResponder會被指向所在的controller,而controller的nextResponder會被指向self.view的superView镊辕,這樣油够,整個app就通過nextResponder串成了一條鏈,也就是我們所說的響應鏈征懈。所以響應鏈就是一條虛擬的鏈石咬,并沒有一個對象來專門存儲這樣的一條鏈,而是通過UIResponder的屬性串連起來的受裹。如下圖:
##Hit-Testing View
文章開頭說到有iOS三種event類型碌补,事件傳遞中UIWindow會根據(jù)不同的event,用不同的方式尋找initial object棉饶,initial object決定于當前的事件類型厦章。比如Touch Event,UIWindow會首先試著把事件傳遞給事件發(fā)生的那個view照藻,就是下文要說的hit-testview袜啃。對于Motion和Remote Event,UIWindow會把例如震動或者遠程控制的事件傳遞給當前的firstResponder幸缕。下面主要講Touch Event的hit-testview群发。
有了事件響應鏈,接下來的事情就是尋找響應事件的具體響應者了发乔,我們稱著為:Hit-Testing View熟妓,尋找這個View的過程我們稱著為Hit-Test。
那么什么是Hit-Test呢栏尚,我們可以把它理解為一個探測器起愈,通過這個探測器我們可以找到并判斷手指是否點擊在某個視圖上面,換句話說就是通過Hit-Test可以找到手指點擊到的處于屏幕最前面的那個UIView。
在解釋Hit-Test是怎么工作之前抬虽,先來看看它是什么時候被調(diào)用的官觅。前面說Hit-Test是一個探測器,那么在代碼里面其實就是一個函數(shù)阐污,UIView有如下兩個方法:
每當手指接觸屏幕休涤,UIApplication接收到手指的事件之后,就會去調(diào)用UIWindow的hitTest:withEvent:笛辟,看看當前點擊的點是不是在window內(nèi)功氨,如果是則繼續(xù)依次調(diào)用subView的hitTest:withEvent:方法,直到找到最后需要的view隘膘。調(diào)用結(jié)束并且hit-test view確定之后疑故,這個view和view上面依附的手勢,都會和一個UITouch的對象關(guān)聯(lián)起來弯菊,這個UITouch會作為事件傳遞的參數(shù)之一纵势,我們可以看到UITouch頭文件里面有一個view和gestureRecognizers的屬性,就是hitTest view和它的手勢管钳。
現(xiàn)在知道Hit-Test是什么時候調(diào)用了钦铁,那么接下來看看它是怎么工作的。Hit-Test是采用遞歸的方法從view層級的根節(jié)點開始遍歷才漆,看看下面這張圖:
UIWindow有一個MianVIew牛曹,MainView里面有三個subView:view A、view B醇滥、view C黎比,他們各自有兩個subView,他們層級關(guān)系是:view A在最下面鸳玩,view B中間阅虫,view C最上(也就是addSubview的順序,越晚add進去越在上面)不跟,其中view A和view B有一部分重疊颓帝。如果手指在view B.1和view A.2重疊的上面點擊,按照上面說的遞歸方式窝革,順序如下圖所示:
遞歸是向界面的根節(jié)點UIWindow發(fā)送hitTest:withEvent:消息開始的购城,從這個消息返回的是一個UIView,也就是手指當前位置最前面的那個 hittest view虐译。 當向UIWindow發(fā)送hitTest:withEvent:消息時瘪板,hitTest:withEvent:里面所做的事,就是判斷當前的點擊位置是否在window里面漆诽,如果在則遍歷window的subview然后依次對subview發(fā)送hitTest:withEvent:消息(注意這里給subview發(fā)送消息是根據(jù)當前subview的index順序侮攀,index越大就越先被訪問)史侣。如果當前的point沒有在view上面,那么這個view的subview也就不會被遍歷了魏身。當事件遍歷到了view B.1,發(fā)現(xiàn)point在view B.1里面蚪腐,并且view B.1沒有subview箭昵,那么他就是我們要找的hittest view了,找到之后就會一路返回直到根節(jié)點回季,而view B之后的view A也不會被遍歷了家制。
一圖勝千言:
注意hitTest里面是有判斷當前的view是否支持點擊事件,比如userInteractionEnabled泡一、hidden颤殴、alpha等屬性,都會影響一個view是否可以相應事件鼻忠,如果不響應則直接返回nil涵但。 我們留意到還有一個pointInside:withEvent:方法,這個方法跟hittest:withEvent:一樣都是UIView的一個方法帖蔓,通過他開判斷point是否在view的frame范圍內(nèi)矮瘟。如果這些條件都滿足了,那么遍歷就可以繼續(xù)往下走了塑娇,代碼表現(xiàn)大概如下:
###Hit-Test的應用
####一澈侠、擴大view的點擊區(qū)域 一個按鈕尺寸是10pt*10pt,如果要擴大按鈕的點擊區(qū)域(按鈕四周之外的10pt也可以響應按鈕的事件)埋酬,可以怎么做呢哨啃?或許重寫hittest:withEvent:是個好辦法,hitest就是返回可以響應事件的view写妥,如果我們在button的子類里面重寫它拳球,在方法里面判斷如果point在button的frame之外的10pt內(nèi),就返回button自己耳标。
####二醇坝、將事件傳遞給兄弟view 如上面第一個圖,如果需要是需要view A響應事件而不是B(即使點在重疊的部分)次坡,什么都不做的話呼猪,當點擊在重疊的時候,A是不能響應事件的砸琅,除非B的userInteractionEnabled為NO并且者B沒有任何事件的響應函數(shù)宋距。這個時候通過重寫B(tài)的hittest可以解決這個問題,在B的hittest里面直接返回nil就行了症脂。
####三谚赎、將事件傳遞給subview 如下圖淫僻,藍色的scrollView設置pagingEnabled使得image停止?jié)L動后都會固定在居中的位置,如果在scrollView的左邊或者右邊活動壶唤,發(fā)現(xiàn)scrollView是無法滾動的雳灵,原因就是hittest里面沒有滿足pointInSide這個條件,scrollView的bound只有藍色的區(qū)域闸盔。這個時候重寫UIView的hittest:withEvent:悯辙,然后返回scrollView即可解決問題。
##事件的傳遞
有了響應鏈迎吵,并且找到了第一個響應事件的對象躲撰,接下來就是把事件發(fā)送個這個響應者了。 UIApplication中有個sendEvent:的方法击费,在UIWindow中同樣也可以發(fā)現(xiàn)一個同樣的方法拢蛋。UIApplication是通過這個方法把事件發(fā)送給UIWindow,然后UIWindow通過同樣的接口蔫巩,把事件發(fā)送給hit-testview谆棱。這個我們可以從Time Profiler里面得到證實:
當我點擊了WRBuyBookButton之后,UIWindow會通過一個私有方法圆仔,在里面會去調(diào)用按鈕的touchesBegan和touchesEnded方法础锐,touchesBegan里面有設置按鈕的高亮等之類的動作,這樣就實現(xiàn)了事件的傳遞荧缘。而事件的響應皆警,也就是按鈕上綁定的action,是在touchEnded里面通過調(diào)用UIApplication的sendAction:to:from:forEvent:方法來實現(xiàn)的截粗,至于這個方法里面是怎么去響應action信姓,就只能猜測了(可能是通過oc底層消息機制的相關(guān)接口 objc_msgSend 來發(fā)送消息實現(xiàn)的,可以參考message.h文件)绸罗。如果第一響應者沒有響應這個事件意推,那么就會根據(jù)響應鏈,把事件冒泡傳遞給nextResponder來響應珊蟀。
注意這里是怎么把事件傳遞給nextResponder的呢菊值?拿touch事件來說,UIResponder里面touch四個階段的方法里面育灸,實際上是什么事都沒有做的腻窒,UIView繼承了它進行重寫,重寫的內(nèi)容也是沒有什么東西磅崭,就是把事件傳遞給nextResponder儿子,比如:[self.nextResponder touchesBegan:touches withEvent:event]。所以當一個view或者controller里面沒有重寫touch事件砸喻,那么這個事件就會一直傳遞下去柔逼,直到UIApplication蒋譬,這也就是事件往上冒泡的原理。如果view重寫了touch方法愉适,我們一般會看到的效果是犯助,這個view響應了事件之后,事件就被截斷了(就像JavaScript里面調(diào)用e.stopPropagation())维咸,它的nextResponder不會收到這個事件也切,即使重寫了nextResponder的touch方法。這個時候如果想事件繼續(xù)傳遞下去腰湾,可以調(diào)用[super touchesBegan:touches withEvent:event],不建議直接調(diào)[self.nextResponder touchesBegan:touches withEvent:event]疆股。
##關(guān)于UIScrollView的事件
先說一個現(xiàn)象费坊,我們平時加到UIScrollView(或者UITableView和UICollection)上面的UIButton,即使有設置highLighted的樣式旬痹,點擊的時候卻發(fā)現(xiàn)這個樣式老是不出來附井,但是按鈕的事件明明可以響應的,很詭異两残。
后來才知道永毅,UIScrollView因為要滾動,所以對事件做了特殊的處理: 當UIScrollView接收到事件之后人弓,會暫時劫持當前的事件300毫秒沼死,如果300毫秒之后手指還沒有滾動,則認為你放棄滾動崔赌,放棄對事件的劫持并往下傳遞意蛀,但是從Time Profiler看到此時按鈕并不是調(diào)用自身的touch方法,而是調(diào)用自身綁定的手勢的touch事件健芭,由于按鈕的highLighted樣式是寫在按鈕的touch方法上的县钥,所以這個這個時候就看不到高亮了。但是長按按鈕缺可以讓按鈕有高亮的狀態(tài)慈迈,這個就不太清楚為什么了若贮,因為從Time Profiler里面看按鈕的touchesBegan好像還是沒有被調(diào)。 如果300毫秒之內(nèi)手指滾動了痒留,則響應滾動的事件谴麦,事件就不會繼續(xù)傳給subView了,也就是不會繼續(xù)調(diào)用按鈕上手勢的touch方法了伸头。
可以通過UIScrollView的一個屬性來解決這個問題:delaysContentTouches细移,意思是是否需要延遲處理事件的傳遞,默認是NO熊锭。把delaysContentTouches設置為YES之后弧轧,一切看起來挺好的雪侥,按鈕終于有高亮樣式了哈哈哈,但是發(fā)現(xiàn)另一個問題:如果手指點擊在按鈕上面并滾動UIScrollView精绎,發(fā)現(xiàn)怎么也滾動不了速缨。原因是當手指點擊UIScrollView并在滾動之前,如果subView接收并且可以響應事件(delaysContentTouches設置為YES)代乃,則事件響應鏈會在subView響應事件之后就截斷旬牲,即UIScrollView本身不會響應到此事件,不會發(fā)生滾動搁吓≡可以設置canCancelContentTouches為YES來讓UIScrollView可以滾動,與之類似的還有一個touchesShouldCancelInContentView:接口堕仔,可以根據(jù)參數(shù)view來更方便的判斷是否需要cancel擂橘,如果有需要可以在UIScrollView的子類里面重寫這個接口。
這一塊里面的具體實現(xiàn)原理我們都不知道摩骨,水太深了通贞,只能通過Time Profiler來看到一些大概的實現(xiàn),我們也沒必要去深究恼五,大方向理解就好了昌罩。真的有興趣的同學也可以去研究研究。
原文地址:http://zhoon.github.io/ios/2015/04/12/ios-event.html