[轉載]cocos2d-觸摸分發原理

 

本文由泰然翻譯組組長 TXX_糖炒小蝦 原創,版權全部,轉載請註明出處並通知做者和泰然!api

原做 http://www.ityran.com/archives/1326/comment-page-1數組

觸摸是iOS程序的精髓所在,良好的觸摸體驗能讓iOS程序獲得很是好的效果,例如Clear。
鑑於同窗們只會用cocos2d的 CCTouchDispatcher 的 api 但並不知道工做原理,但瞭解觸摸分發的過程是極爲重要的。畢竟涉及到權限、兩套協議等的各類分發。因而我寫了這篇文章來拋磚引玉。緩存

本文以cocos2d-iphone源代碼爲講解。cocos2d-x 於此相似,就不過多贅述了。app

零、cocoaTouch的觸摸
在講解cocos2d觸摸協議以前,我以爲我有必要提一下CocoaTouch那四個方法。畢竟cocos2d的Touch Delegate 也是經過這裏接入的。iphone

 

  1. -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
  2. -(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
  3. -(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
  4. -(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;

一、一個UITouch的生命週期
一個觸摸點會被包裝在一個UITouch中,在TouchesBegan的時候建立,在Cancelled或者Ended的時候被銷燬。也就是說,一個觸摸點在這四個方法中內存地址是相同的,是同一個對象。
二、UIEvent
這是一個常常被大夥兒忽視的東西,基本上沒見過有誰用過,不過這個東西的確不經常使用。能夠理解爲UIEvent是UITouch的一個容器。
你能夠經過UIEvent的allTouches方法來得到當前全部觸摸事件。那麼和傳入的那個NSSet有什麼區別呢?
那麼來設想一個狀況,在開啓多點支持的狀況下,我有一個手指按在屏幕上,既不移動也不離開。而後,又有一隻手指按下去。
這時TouchBegan會被觸發,它接到的NSSet的Count爲1,僅有一個觸摸點。
可是UIEvent的alltouches 倒是2,也就是說那個按在屏幕上的手指的觸摸信息,是能夠經過此方法獲取到的,並且他的狀態是UITouchPhaseStationary
三、關於Cancelled的誤區
有不少人認爲,手指移出屏幕、或移出那個View的Frame 會觸發touchCancelled,這是個很大的誤區。移出屏幕觸發的是touchEned,移出view的Frame不會致使觸摸終止,依然是Moved狀態。
那麼Cancelled是幹什麼用的?
官方解釋:This method is invoked when the Cocoa Touch framework receives a system interruption requiring cancellation of the touch event; for this, it generates a UITouch object with a phase of UITouchPhaseCancel. The interruption is something that might cause the application to be no longer active or the view to be removed from the window
當Cocoa Touch framework 接到系統中斷通知須要取消觸摸事件的時候會調用此方法。同時會將致使一個UITouch對象的phase改成UITouchPhaseCancel。這個中斷每每是由於app長時間沒有響應或者當前view從window上移除了。異步

據我統計,有這麼幾種狀況會致使觸發Cancelled:
一、官方所說長時間無響應,view被移除
二、觸摸的時候來電話,彈出UIAlert View(低電量 短信 推送 之類),按了home鍵。也就是說程序進入後臺。
三、屏幕關閉,觸摸的時候,某種緣由致使距離傳感器工做,例如臉靠近。
四、手勢的權限蓋掉了Touch, UIGestureRecognizer 有一個屬性:函數

  1. @property(nonatomic) BOOL cancelsTouchesInView;
  2. // default is YES. causes touchesCancelled:withEvent: to be sent to the view for all touches recognized as part of this gesture immediately before the action method is called

關於CocoaTouch就說到這裏,CocoaTouch的Touch和Gesture混用 我會在未來的教程中寫明。
1、TouchDelegate的接入。優化

衆所周知CCTouchDelegate是經過CocoaTouch的API接入的,那麼是從哪裏接入的呢?咱們是知道cocos2d是跑在一個view上的,這個view 就是 EAGLView 可在cocos2d的Platforms的iOS文件夾中找到。
在它的最下方能夠看到,他將上述四個api傳入了一個delegate。這個delegate是誰呢?
沒錯就是CCTouchDispatcherui

但縱覽整個EAGLView的.m文件,你是找不到任何和CCTouchDispatcher有關的東西的。
那麼也就是說在初始化的時候載入的咯?this

EAGLView的初始化在appDelegate中,但依然沒看到有關CCTouchDispatcher 有關的東西,但能夠留意一句話:

  1. [director setOpenGLView:glView];

點開後能夠發現

  1. CCTouchDispatcher*touchDispatcher =[CCTouchDispatcher sharedDispatcher];
  2. [openGLView_ setTouchDelegate: touchDispatcher];
  3. [touchDispatcher setDispatchEvents: YES];

呵呵~ CCTouchDispatcher 被發現了!

2、兩套協議
CCTouchDispatcher 提供了兩套協議。

  1. @protocolCCTargetedTouchDelegate
  2. -(BOOL)ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event;
  3. @optional
  4. -(void)ccTouchMoved:(UITouch*)touch withEvent:(UIEvent*)event;
  5. -(void)ccTouchEnded:(UITouch*)touch withEvent:(UIEvent*)event;
  6. -(void)ccTouchCancelled:(UITouch*)touch withEvent:(UIEvent*)event;
  7. @end
  8. @protocolCCStandardTouchDelegate
  9. @optional
  10. -(void)ccTouchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
  11. -(void)ccTouchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
  12. -(void)ccTouchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
  13. -(void)ccTouchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;
  14. @end

與之對應的還有兩個在CCTouchDispatcher 中的添加操做

  1. -(void) addStandardDelegate:(id)delegate priority:(int)priority;
  2. -(void) addTargetedDelegate:(id)delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;

其中StandardTouchDelegate 單獨使用的時候用法和 cocoaTouch 相同。
咱們這裏重點說一下CCTargetedTouchDelegate
在頭文件的註釋中能夠看到:
使用它的好處:
一、不用去處理NSSet, 分發器會將它拆開,每次調用你都能精確的拿到一個UITouch
二、你能夠在touchbegan的時候retun yes,這樣以後touch update 的時候 再得到到的touch 確定是它本身的。這樣減輕了你對多點觸控時的判斷。

除此以外還有
三、TargetedTouchDelegate支持SwallowTouch 顧名思義,若是這個開關打開的話,比他權限低的handler 是收不到 觸摸響應的,順帶一提,CCMenu 就是開了Swallow 而且權限爲-128(權限是越小越好)

四、 CCTargetedTouchDelegate 的級別比 CCStandardDelegate 高,高在哪裏了呢? 在後文講分發原理的時候 我會說具體說明。

3、CCTouchHandler

在說分發以前,還要介紹下這個類的做用。
簡而言之呢,這個類就是用於存儲你的向分發器註冊協議時的參數們。
類指針,類所擁有的那幾個函數們,以及觸摸權限。

只不過在 CCTargetedTouchHandler 中還有這麼一個東西

  1. @property(nonatomic,readonly)NSMutableSet*claimedTouches;

這個東西就是記錄當前這個delegate中 拿到了多少 Touches 罷了。
只是想在這裏說一點:
UITouch只要手指按在屏幕上 不管是滑動 也好 開始began 也好 finished 也好
對於一次touch操做,從開始到結束 touch的指針是不變的.

4、觸摸分發
前面鋪墊這麼多,終於講到重點了。
這裏我就結合這他的代碼說好了。

首先先說dispatcher定義的數據成員

  1. NSMutableArray*targetedHandlers;
  2. NSMutableArray*standardHandlers;
  3.  
  4. BOOL locked;
  5. BOOL toAdd;
  6. BOOL toRemove;
  7. NSMutableArray*handlersToAdd;
  8. NSMutableArray*handlersToRemove;
  9. BOOL toQuit;
  10.  
  11. BOOL dispatchEvents;
  12.  
  13. // 4, 1 for each type of event
  14. struct ccTouchHandlerHelperData handlerHelperData[kCCTouchMax];

開始那兩個 數組 顧名思義是存handlers的 不用多說

以後下面那一段的東西是用於線程間數據修改時的標記。
提一下那個lock爲真的時候 表明當前正在進行觸摸分發

而後是總開關
最後就是個helper 。。

而後說以前提到過的那兩個插入方法

  1. -(void) addStandardDelegate:(id)delegate priority:(int)priority;
  2. -(void) addTargetedDelegate:(id)delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;

就是按照priority插入對應的數組中。
但要注意一點:當前若正在進行事件分發,是不進行插入的。取而代之的是放到一個緩存數組中。等觸摸分發結束後才加入其中。

在講分發前,再提一個函數

  1. -(void) setPriority:(int) priority forDelegate:(id)delegate

調整權限,講它的目的是爲了講它中間包含的兩個方法一個c函數,

  1. -(CCTouchHandler*) findHandler:(id)delegate;-(void) rearrangeHandlers:(NSMutableArray*)array;NSComparisonResult sortByPriority(id first, id second,void*context);

調整權限的過程就是,先找到那個handler的指針,修改它的數值,而後對兩個數組從新排序。 這裏有幾個細節: 一、findHandler 是先找 targeted 再找standard 且找到了就 return。也就是說 若是 一個類既註冊了targeted又註冊了standard,這裏會出現衝突。 二、排序的比較器函數 只比較權限,其餘一概不考慮。 在dispatcher.m的文件中末,能夠看到EAGLTouchDelegate 全都指向了

  1. -(void) touches:(NSSet*)touches withEvent:(UIEvent*)event withTouchType:(unsignedint)idx

這個方法。

他就是整個 dispatcher的核心。
下面咱們來分段講解下。
最開始

  1. id mutableTouches;
  2. locked = YES;
  3.  
  4. // optimization to prevent a mutable copy when it is not necessary
  5. unsignedint targetedHandlersCount =[targetedHandlers count];
  6. unsignedint standardHandlersCount =[standardHandlers count];
  7. BOOL needsMutableSet =(targetedHandlersCount && standardHandlersCount);
  8.  
  9. mutableTouches =(needsMutableSet ?[touches mutableCopy]: touches);
  10.  
  11. struct ccTouchHandlerHelperData helper = handlerHelperData[idx];

首先開啓了鎖,以後是一個小優化。
就是說 若是 target 和 standard 這兩個數組中 有一個爲空的話 就不用 將傳入的 set copy 一遍了。

下面開始正題
targeted delegate 分發!

  1. if( targetedHandlersCount >0){
  2. for(UITouch*touch in touches ){
  3. for(CCTargetedTouchHandler*handler in targetedHandlers){
  4.  
  5. BOOL claimed = NO;
  6. if( idx == kCCTouchBegan ){
  7. claimed =[handler.delegate ccTouchBegan:touch withEvent:event];
  8. if( claimed )
  9. [handler.claimedTouches addObject:touch];
  10. }
  11.  
  12. // else (moved, ended, cancelled)
  13. elseif([handler.claimedTouches containsObject:touch]){
  14. claimed = YES;
  15. if( handler.enabledSelectors & helper.type )
  16. [handler.delegate performSelector:helper.touchSel withObject:touch withObject:event];
  17.  
  18. if( helper.type &(kCCTouchSelectorCancelledBit | kCCTouchSelectorEndedBit))
  19. [handler.claimedTouches removeObject:touch];
  20. }
  21.  
  22. if( claimed && handler.swallowsTouches ){
  23. if( needsMutableSet )
  24. [mutableTouches removeObject:touch];
  25. break;
  26. }
  27. }
  28. }
  29. }

其實分發很簡單,先枚舉每一個觸摸點,而後枚舉targeted數組中的handler
若當前觸摸是 began 的話 那麼就 運行 touchbegan函數 若是 touch began return Yes了 那麼證實這個觸摸被claim了。加入handler的那個集合中。
若當前觸摸不是began 那麼判斷 handler那個集合中有沒有這個 UItouch 若是有 證實 以前的touch began return 了Yes 能夠繼續update touch。 若操做是結束或者取消,就從set中把touch刪掉。

最後這點很重要 當前handlerclaim且設置爲吞掉觸摸的話,會刪除standardtouchdelegate中對應的觸摸點,而且終止循環。

targeted全部觸摸事件分發完後開始進行standard 觸摸事件分發。

按這個次序咱們能夠發現…
一、再次提起swallow,一旦targeted設置爲swallow 比它權限低的 以及 standard 不管是多高的權限 全都收不到觸摸分發。
二、standard的觸摸權限 設置爲 負無窮(最高) 也沒有 targeted的正無窮(最低)權限高。
三、觸摸分發,只和權限有關,和層的高度(zOrder)徹底不要緊,哪怕是一樣的權限,也有可能低下一層先收到觸摸,上面那層才接到。權限相同時數組裏是亂序的,非插入順序。

最後,關閉鎖
開始判斷在數據分發的時候有沒有發生 添加 刪除 清空handler的狀況。
結束分發

注意,事件分發後的異步處理信息會出現幾個有意思的反作用
一、刪除的時候 retainCnt +1由於要把handler暫時加入緩存數組中。
雖然說是暫時的,可是會混淆你的調試。
例如:

  1. -(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event
  2. {
  3. NSLog(@"button retainCnt = ", button.retainCount);
  4. [[CCTouchDispatcher sharedDispatcher] removeDelegate:button];
  5. NSLog(@"button retainCnt = ", button.retainCount);
  6. }

若是你內存管理作得好的話,應該是 輸出 2 和 3
2 是在 addchild 和 dispatcher中添加了。
3 是在 cache 中又被添加一次。

二、有些操做會失去你想要表達的效果。
例如一個你寫了個ScrollView 上面有一大塊menu。你想在手指拖拽view的時候 屏蔽掉 那個menu的響應。
也許你會這麼作:
1)讓scrollview的權限比menu還要高,並設爲不吞掉觸摸。
2)滑動的時候,scrollview確定會先收到觸摸,這時取消掉menu的響應。
3)觸摸結束還,還原menu響應

但實際上第二步的時候 menu 仍是會收到響應的,會把menu的item變成selected狀態。而且須要手動還原

樣例代碼以下:

  1. -(id) init
  2. {
  3. // always call "super" init
  4. // Apple recommends to re-assign "self" with the "super" return value
  5. if((self=[super init])){
  6.  
  7. CCSprite* sprite =[CCSprite spriteWithFile:@"Icon.png"];
  8. CCSprite* sprite1 =[CCSprite spriteWithFile:@"Icon.png"];
  9. sprite1.color = ccRED;
  10.  
  11. CCMenuItem* item =[CCMenuItemSprite itemFromNormalSprite:sprite
  12. selectedSprite:sprite1
  13. block:^(id sender){
  14. AudioServicesPlayAlertSound(1000);
  15. }];
  16.  
  17. item.position = ccp(100,100);
  18. CCMenu* menu =[CCMenu menuWithItems:item,nil];
  19. menu.position = ccp(0,0);
  20. menu.tag =1025;
  21. [self addChild:menu];
  22.  
  23. [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:-129 swallowsTouches:NO];
  24. }
  25. returnself;
  26. }
  27.  
  28. -(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event
  29. {
  30. return YES;
  31. }
  32.  
  33. -(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent*)event
  34. {
  35. CCMenu*menu =(CCMenu*)[self getChildByTag:1025];
  36. menu.isTouchEnabled = NO;
  37. }
  38.  
  39. -(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent*)event
  40. {
  41. CCMenu*menu =(CCMenu*)[self getChildByTag:1025];
  42. menu.isTouchEnabled = YES;
  43. }

三、須要注意的一點是,TouchTargetedDelegate 並無屏蔽掉多點觸摸,而是將多點離散成了單點,同時傳遞過來了。

也就是說,每個觸摸點都會走UITouch LifeCircle ,只是由於在正常狀況下NSSet提取出來的信息順序相同,使得你每次操做看起來只是最後一個觸摸點生效了。可是若是用戶「手賤」,多指觸摸,並不一樣時擡起所有手指,你將收到諸如start(-move)-end-(move)-end 之類的狀況。若開啓了多點觸控支持,必定要考慮好這點!不然可能會被用戶玩出來一些奇怪的bug…

相關文章
相關標籤/搜索