Cocos2D 觸摸分發原理

form:http://article.ityran.com/archives/1326

觸摸是iOS程序的精髓所在,良好的觸摸體驗能讓iOS程序得到非常好的效果,例如Clear。

鑑於同學們只會用cocos2d的 CCTouchDispatcher 的 api 但並不知道工作原理,但瞭解觸摸分發的過程是極爲重要的。畢竟涉及到權限、兩套協議等的各種分發。於是我寫了這篇文章來拋磚引玉。

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


零、cocoaTouch的觸摸

在講解cocos2d觸摸協議之前,我覺得我有必要提一下CocoaTouch那四個方法。畢竟cocos2d的Touch Delegate 也是通過這裏接入的。

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

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

1、一個UITouch的生命週期

一個觸摸點會被包裝在一個UITouch中,在TouchesBegan的時候創建,在Cancelled或者Ended的時候被銷燬。也就是說,一個觸摸點在這四個方法中內存地址是相同的,是同一個對象。

2、UIEvent

這是一個經常被大夥兒忽視的東西,基本上沒見過有誰用過,不過這個東西的確不常用。可以理解爲UIEvent是UITouch的一個容器。

你可以通過UIEvent的allTouches方法來獲得當前所有觸摸事件。那麼和傳入的那個NSSet有什麼區別呢?

那麼來設想一個情況,在開啓多點支持的情況下,我有一個手指按在屏幕上,既不移動也不離開。然後,又有一隻手指按下去。

這時TouchBegan會被觸發,它接到的NSSet的Count爲1,僅有一個觸摸點。

但是UIEvent的alltouches 卻是2,也就是說那個按在屏幕上的手指的觸摸信息,是可以通過此方法獲取到的,而且他的狀態是UITouchPhaseStationary

3、關於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:

1、官方所說長時間無響應,view被移除

2、觸摸的時候來電話,彈出UIAlert View(低電量 短信 推送 之類),按了home鍵。也就是說程序進入後臺。

3、屏幕關閉,觸摸的時候,某種原因導致距離傳感器工作,例如臉靠近。

4、手勢的權限蓋掉了Touch, UIGestureRecognizer 有一個屬性:

@property(nonatomic) BOOL cancelsTouchesInView;

// 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混用 我會在將來的教程中寫明。


一、TouchDelegate的接入。

 

衆所周知CCTouchDelegate是通過CocoaTouch的API接入的,那麼是從哪裏接入的呢?我們是知道cocos2d是跑在一個view上的,這個view 就是 EAGLView 可在cocos2d的Platforms的iOS文件夾中找到。

在它的最下方可以看到,他將上述四個api傳入了一個delegate。這個delegate是誰呢?

沒錯就是CCTouchDispatcher

但縱覽整個EAGLView的.m文件,你是找不到任何和CCTouchDispatcher有關的東西的。

那麼也就是說在初始化的時候載入的咯?

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

[director setOpenGLView:glView];

點開後可以發現

CCTouchDispatcher *touchDispatcher = [CCTouchDispatcher sharedDispatcher];

[openGLView_ setTouchDelegate: touchDispatcher];

[touchDispatcher setDispatchEvents: YES];

呵呵~ CCTouchDispatcher 被發現了!

 

二、兩套協議

CCTouchDispatcher 提供了兩套協議。

@protocol CCTargetedTouchDelegate

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event;

@optional

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event;

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event;

- (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event;

@end

@protocol CCStandardTouchDelegate

@optional

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end

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

 -(void) addStandardDelegate:(id) delegate priority:(int)priority;

-(void) addTargetedDelegate:(id) delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;

其中StandardTouchDelegate 單獨使用的時候用法和 cocoaTouch 相同。

我們這裏重點說一下CCTargetedTouchDelegate

在頭文件的註釋中可以看到:

使用它的好處:

1、不用去處理NSSet, 分發器會將它拆開,每次調用你都能精確的拿到一個UITouch

2、你可以在touchbegan的時候retun yes,這樣之後touch update 的時候 再獲得到的touch 肯定是它自己的。這樣減輕了你對多點觸控時的判斷。

除此之外還有

3、TargetedTouchDelegate支持SwallowTouch 顧名思義,如果這個開關打開的話,比他權限低的handler 是收不到 觸摸響應的,順帶一提,CCMenu 就是開了Swallow 並且權限爲-128(權限是越小越好)

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

 

三、CCTouchHandler

在說分發之前,還要介紹下這個類的作用。

簡而言之呢,這個類就是用於存儲你的向分發器註冊協議時的參數們。

類指針,類所擁有的那幾個函數們,以及觸摸權限。

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

@property(nonatomic, readonly) NSMutableSet *claimedTouches;

這個東西就是記錄當前這個delegate中 拿到了多少 Touches 罷了。

只是想在這裏說一點:

UITouch只要手指按在屏幕上 無論是滑動 也好 開始began 也好 finished 也好

對於一次touch操作,從開始到結束 touch的指針是不變的.

 

四、觸摸分發

前面鋪墊這麼多,終於講到重點了。

這裏我就結合這他的代碼說好了。

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

NSMutableArray *targetedHandlers;

NSMutableArray *standardHandlers;

BOOL locked;

BOOL toAdd;

BOOL toRemove;

NSMutableArray *handlersToAdd;

NSMutableArray *handlersToRemove;

BOOL toQuit;

BOOL dispatchEvents;

// 4, 1 for each type of event

struct ccTouchHandlerHelperData handlerHelperData[kCCTouchMax];
 

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

之後下面那一段的東西是用於線程間數據修改時的標記。

提一下那個lock爲真的時候 代表當前正在進行觸摸分發

然後是總開關

最後就是個helper 。。

 

然後說之前提到過的那兩個插入方法

-(void) addStandardDelegate:(id) delegate priority:(int)priority;

-(void) addTargetedDelegate:(id) delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;

就是按照priority插入對應的數組中。

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

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

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

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

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

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

-(void) touches:(NSSet*)touches withEvent:(UIEvent*)event withTouchType:(unsigned int)idx

這個方法。

他就是整個 dispatcher的核心。

下面我們來分段講解下。

最開始

  1. id mutableTouches; 
  2.  
  3.     locked = YES
  4.  
  5.   
  6.  
  7.     // optimization to prevent a mutable copy when it is not necessary 
  8.  
  9.     unsigned int targetedHandlersCount = [targetedHandlers count]; 
  10.  
  11.     unsigned int standardHandlersCount = [standardHandlers count]; 
  12.  
  13.     BOOL needsMutableSet = (targetedHandlersCount && standardHandlersCount); 
  14.  
  15.   
  16.  
  17.     mutableTouches = (needsMutableSet ? [touches mutableCopy] : touches); 
  18.  
  19.   
  20.  
  21.     struct ccTouchHandlerHelperData helper = handlerHelperData[idx]; 

首先開啓了鎖,之後是一個小優化。

就是說 如果 target 和 standard 這兩個數組中 有一個爲空的話 就不用 將傳入的 set copy 一遍了。

 

下面開始正題

targeted delegate 分發!

  1. if( targetedHandlersCount > 0 ) { 
  2.  
  3.         for( UITouch *touch in touches ) { 
  4.  
  5.             for(CCTargetedTouchHandler *handler in targetedHandlers) { 
  6.  
  7.   
  8.  
  9.                 BOOL claimed = NO
  10.  
  11.                 if( idx == kCCTouchBegan ) { 
  12.  
  13.                     claimed = [handler.delegate ccTouchBegan:touch withEvent:event]; 
  14.  
  15.                     if( claimed ) 
  16.  
  17.                         [handler.claimedTouches addObject:touch]; 
  18.  
  19.                 }  
  20.  
  21.   
  22.  
  23.                 // else (moved, ended, cancelled) 
  24.  
  25.                 else if( [handler.claimedTouches containsObject:touch] ) { 
  26.  
  27.                     claimed = YES
  28.  
  29.                     if( handler.enabledSelectors & helper.type ) 
  30.  
  31.                         [handler.delegate performSelector:helper.touchSel withObject:touch withObject:event]; 
  32.  
  33.   
  34.  
  35.                     if( helper.type & (kCCTouchSelectorCancelledBit | kCCTouchSelectorEndedBit) ) 
  36.  
  37.                         [handler.claimedTouches removeObject:touch]; 
  38.  
  39.                 } 
  40.  
  41.   
  42.  
  43.                 if( claimed && handler.swallowsTouches ) { 
  44.  
  45.                     if( needsMutableSet ) 
  46.  
  47.                         [mutableTouches removeObject:touch]; 
  48.  
  49.                     break; 
  50.  
  51.                 } 
  52.  
  53.             } 
  54.  
  55.         } 
  56.  
  57.     } 

其實分發很簡單,先枚舉每個觸摸點,然後枚舉targeted數組中的handler

若當前觸摸是 began 的話 那麼就 運行 touchbegan函數 如果 touch began return Yes了 那麼證明這個觸摸被claim了。加入handler的那個集合中。

若當前觸摸不是began 那麼判斷 handler那個集合中有沒有這個 UItouch 如果有 證明 之前的touch began return 了Yes 可以繼續update touch。 若操作是結束或者取消,就從set中把touch刪掉。

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

targeted所有觸摸事件分發完後開始進行standard 觸摸事件分發。

 

按這個次序我們可以發現…

1、再次提起swallow,一旦targeted設置爲swallow 比它權限低的 以及 standard 無論是多高的權限 全都收不到觸摸分發。

2、standard的觸摸權限 設置爲 負無窮(最高) 也沒有 targeted的正無窮(最低)權限高。

3、觸摸分發,只和權限有關,和層的高度(zOrder)完全沒關係,哪怕是同樣的權限,也有可能低下一層先收到觸摸,上面那層才接到。權限相同時數組裏是亂序的,非插入順序。

 

最後,關閉鎖

開始判斷在數據分發的時候有沒有發生 添加 刪除 清空handler的情況。

結束分發

注意,事件分發後的異步處理信息會出現幾個有意思的副作用
 

1、刪除的時候 retainCnt +1因爲要把handler暫時加入緩存數組中。

雖說是暫時的,但是會混淆你的調試。

例如:

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

如果你內存管理做得好的話,應該是 輸出 2 和 3

2 是在 addchild 和 dispatcher中添加了。

3 是在 cache 中又被添加一次。

 

2、有些操作會失去你想要表達的效果。

例如一個你寫了個ScrollView 上面有一大塊menu。你想在手指拖拽view的時候 屏蔽掉 那個menu的響應。

也許你會這麼做:

1)讓scrollview的權限比menu還要高,並設爲不吞掉觸摸。

2)滑動的時候,scrollview肯定會先收到觸摸,這時取消掉menu的響應。

3)觸摸結束還,還原menu響應

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

樣例代碼如下:

  1. -(id) init 
  2.  
  3.  
  4.     // always call "super" init 
  5.  
  6.     // Apple recommends to re-assign "self" with the "super" return value 
  7.  
  8.     if( (self=[super init])) { 
  9.  
  10.   
  11.  
  12.         CCSprite* sprite = [CCSprite spriteWithFile:@"Icon.png"]; 
  13.  
  14.         CCSprite* sprite1 = [CCSprite spriteWithFile:@"Icon.png"]; 
  15.  
  16.         sprite1.color = ccRED
  17.  
  18.   
  19.  
  20.         CCMenuItem* item = [CCMenuItemSprite itemFromNormalSprite:sprite 
  21.  
  22.                                                    selectedSprite:sprite1 
  23.  
  24.                             block:^(id sender) { 
  25.  
  26.                                 AudioServicesPlayAlertSound(1000); 
  27.  
  28.                             }]; 
  29.  
  30.   
  31.  
  32.         item.position = ccp(100, 100); 
  33.  
  34.         CCMenu* menu = [CCMenu menuWithItems:item, nil]; 
  35.  
  36.         menu.position = ccp(0, 0); 
  37.  
  38.         menu.tag = 1025
  39.  
  40.         [self addChild:menu]; 
  41.  
  42.   
  43.  
  44.         [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:-129 swallowsTouches:NO]; 
  45.  
  46.     } 
  47.  
  48.     return self; 
  49.  
  50.  
  51.   
  52.  
  53. - (BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event 
  54.  
  55.  
  56.     return YES; 
  57.  
  58.  
  59.   
  60.  
  61. - (void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event 
  62.  
  63.  
  64.     CCMenu*menu = (CCMenu*) [self getChildByTag:1025]; 
  65.  
  66.     menu.isTouchEnabled = NO
  67.  
  68.  
  69.   
  70.  
  71. - (void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event 
  72.  
  73.  
  74.     CCMenu*menu = (CCMenu*) [self getChildByTag:1025]; 
  75.  
  76.     menu.isTouchEnabled = YES
  77.  


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

也就是說,每一個觸摸點都會走UITouch LifeCircle ,只是因爲在正常情況下NSSet提取出來的信息順序相同,使得你每次操作看起來只是最後一個觸摸點生效了。

但是如果用戶“手賤”,多指觸摸,並不同時擡起全部手指,你將收到諸如start(-move)-end-(move)-end 之類的情況。

若開啓了多點觸控支持,一定要考慮好這點!否則可能會被用戶玩出來一些奇怪的bug…


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章