iOS事件分發機制(一) hit-Testing

發表於2015-04-17

iOS中的事件大概分為三種,分別是 Milti-Touch Events, Motion Events 和Remote Control Events(events for controlling multimedia)。

本文將主要針對TouchEvents的分發,做一個詳細的介紹。先丟擲一個問題,文章的後續部分會對問題進行解答:iOS7原生的自帶NavigationController可以實現從最左側拖動PopViewController(大約13pt),不管當前可見的ViewController有沒有其他的滑動手勢或者事件,這是為什麼?如何實現。

我們已經處理過太多觸控事件了,比如按鈕的點選事件,一些View的手勢等等。那到底我們點一下螢幕,當前的View是如何知道他被點選了呢,這個就要通過HitTest來確定了

每當我們點選了一下iOS裝置的螢幕,UIKit就會生成一個事件物件UIEvent,然後會把這個Event分發給當前active的app(官方原文說:Then it places the event object in the active app’s event queue.)

告知當前活動的app有事件之後,UIApplication 單例就會從事件佇列中去取最新的事件,然後分發給能夠處理該事件的物件。UIApplication 獲取到Event之後,Application就糾結於到底要把這個事件傳遞給誰,這時候就要依靠HitTest來決定了。

iOS中,hit-Testing的作用就是找出這個觸控點下面的View是什麼,HitTest會檢測這個點選的點是不是發生在這個View上,如果是的話,就會去遍歷這個View的subviews,直到找到最小的能夠處理事件的view,如果整了一圈沒找到能夠處理的view,則返回自身。來一個簡單的圖說明一下

假設我們現在點選到了圖中的E,hit-testing將進行如下步驟的檢測(不包含重寫hit-test並且返回非預設View的情況)

1、觸控點在ViewA內,所以檢查ViewA的Subview B、C

2、觸控點不在ViewB內,觸控點在ViewC內部,所以檢查ViewC的Subview D、E

3、觸控點不在ViewD內,觸控點發生在ViewE內部,並且ViewE沒有subview,所以ViewE屬於ViewA中包含這個點的最小單位,所以ViewE變成了該次觸控事件的hit-TestView

PS.

1、預設的hit-testing順序是按照UIView中Subviews的逆順序

2、如果View的同級別Subview中有重疊的部分,則優先檢查頂部的Subview,如果頂部的Subview返回nil, 再檢查底部的Subview

3、Hit-Test也是比較聰明的,檢測過程中有這麼一點,就是說如果點選沒有發生在某View中,那麼該事件就不可能發生在View的Subview中,所以檢測過程中發現該事件不在ViewB內,也直接就不會檢測在不在ViewF內。也就是說,如果你的Subview設定了clipsToBounds=NO,實際顯示區域可能超出了superView的frame,你點選超出的部分,是不會處理你的事件的,就是這麼任性!

Hit-Test的檢查機制如上所示,當確定了Hit-TestView時,如果當前的application沒有忽略觸控事件 (UIApplication:isIgnoringInteractionEvents),則application就會去分發事件(sendEvent:->keywindow:sendEvent:)

UIView中提供兩個方法用來確定hit-testing View,如下所示 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

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

當一個View收到hitTest訊息時,會呼叫自己的pointInside:withEvent:方法,如果pointInside返回YES,則表明觸控事件發生在我自己內部,則會遍歷自己的所有Subview去尋找最小單位(沒有任何子view)的UIView,如果當前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情況的時候,hitTest就不會呼叫自己的pointInside了,直接返回nil,然後系統就回去遍歷兄弟節點。簡而言之,可以寫成這樣

hit-Test 是事件分發的第一步,就算你的app忽略了事件,也會發生hit-Test。確定了hit-TestView之後,才會開始進行下一步的事件分發。

我們可以利用hit-Test做一些事情,比如我們點選了ViewA,我們想讓ViewB響應,這個時候,我們只需要重寫View’s hitTest方法,返回ViewB就可以了,雖然可能用不到,但是偶爾還是會用到的。大概程式碼如下:

大家可以試一試,上述程式碼在點選上面的按鈕的時候,實際會觸發下面按鈕的事件,不是經常用到,但是也算是漲姿勢了,這裡給大家提供一個Category,來自STKit,這個category的目的就是方便的編寫hitTest方法,由於hitTest方法是override,而不是delegate,所以使用預設的實現方式就比較麻煩。Category如下

程式碼很簡單,就是利用iOS的runtime能力,在hitTest執行之前,插入了一個方法。如果有看不懂的,可以參考我以前的部落格 iOS面向切面程式設計

現在回到我們開始提出的題目,其實題目很簡單,就是簡單的可以把題目轉換為

如果我們觸控點的座標 point.x < 13, 我們就讓hit-Test 返回NavigationController.view, 把所有的事件入口交給他,否則就返回super,該怎麼處理怎麼處理

這樣就能滿足我們的條件,即使當前的VC上面有ScrollView,但是由於點選特定區域的時候,ScrollView根本得不到事件,所以系統會專心處理NavigationController的拖拽手勢,而不是ScrollView的事件,當沒有點選特定區域的時候,NavigationController的手勢不會觸發,系統會專心處理ScrollView的事件,互不影響,大家可以嘗試實現,程式碼量不多。

雖然iOS8新增了UIScreenEdgePanGestureRecognizer 手勢,但是單純的用這個手勢無法解決當前VC上面有ScrollView的問題,有關手勢方面的事件分發,之後的文章會對此進行說明,這裡就不多說了。

當我們確定了HitTestView之後,我們的事件分發就正式開始了,如果hitTestView可以直接處理的,就處理,不能處理的,則交給 The Responder Chain/ GestureRecognizer。後續文章會對分發進行進一步說明。

附上一些測試查詢hitTestView過程中列印的日誌,可以觀察一下:

其中—-表示View的層次結構

相關文章