IOS開發:Cocos2d觸摸分發原理分析

來源:互聯網
上載者:User

IOS開發:Cocos2d觸摸分發原理分析

   觸摸是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;

  BOOLlocked;

  BOOLtoAdd;

  BOOLtoRemove;

  NSMutableArray*handlersToAdd;

  NSMutableArray*handlersToRemove;

  BOOLtoQuit;

  BOOLdispatchEvents;

  // 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; NSComparisonResultsortByPriority(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的核心。

  下面我們來分段講解下。

  最開始

  id mutableTouches;

  locked = YES;

  // optimization to prevent a mutable copy when it is not necessary

  unsigned int targetedHandlersCount = [targetedHandlers count];

  unsigned int standardHandlersCount = [standardHandlers count];

  BOOL needsMutableSet = (targetedHandlersCount && standardHandlersCount);

  mutableTouches = (needsMutableSet ? [touches mutableCopy] : touches);

  struct ccTouchHandlerHelperData helper = handlerHelperData[idx];

  首先開啟了鎖,之後是一個小最佳化。

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

  下面開始正題

  targeted delegate 分發!

  if( targetedHandlersCount > 0 ) {

  for( UITouch *touch in touches ) {

  for(CCTargetedTouchHandler *handler in targetedHandlers) {

  BOOL claimed = NO;

  if( idx == kCCTouchBegan ) {

  claimed = [handler.delegate ccTouchBegan:touch withEvent:event];

  if( claimed )

  [handler.claimedTouches addObject:touch];

  }

  // else (moved, ended, cancelled)

  else if( [handler.claimedTouches containsObject:touch] ) {

  claimed = YES;

  if( handler.enabledSelectors & helper.type )

  [handler.delegate performSelector:helper.touchSel withObject:touch withObject:event];

  if( helper.type & (kCCTouchSelectorCancelledBit | kCCTouchSelectorEndedBit) )

  [handler.claimedTouches removeObject:touch];

  }

  if( claimed && handler.swallowsTouches ) {

  if( needsMutableSet )

  [mutableTouches removeObject:touch];

  break;

  }

  }

  }

  }

  其實分發很簡單,先枚舉每個觸摸點,然後枚舉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暫時加入緩衝數組中。

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

  例如:

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

  {

  NSLog(@"button retainCnt = ", button.retainCount);

  [[CCTouchDispatcher sharedDispatcher] removeDelegate:button];

  NSLog(@"button retainCnt = ", button.retainCount);

  }

  如果你記憶體管理做得好的話,應該是 輸出 2 和 3

  2 是在 addchild 和 dispatcher中添加了。

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

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

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

  也許你會這麼做:

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

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

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

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

  範例代碼如下:

  -(id) init

  {

  // always call "super" init

  // Apple recommends to re-assign "self" with the "super" return value

  if( (self=[super init])) {

  CCSprite* sprite = [CCSprite spriteWithFile:@"Icon.png"];

  CCSprite* sprite1 = [CCSprite spriteWithFile:@"Icon.png"];

  sprite1.color = ccRED;

  CCMenuItem* item = [CCMenuItemSprite itemFromNormalSprite:sprite

  selectedSprite:sprite1

  block:^(id sender) {

  AudioServicesPlayAlertSound(1000);

  }];

  item.position = ccp(100, 100);

  CCMenu* menu = [CCMenu menuWithItems:item, nil];

  menu.position = ccp(0, 0);

  menu.tag = 1025;

  [self addChild:menu];

  [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:-129 swallowsTouches:NO];

  }

  return self;

  }

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

  {

  return YES;

  }

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

  {

  CCMenu*menu = (CCMenu*) [self getChildByTag:1025];

  menu.isTouchEnabled = NO;

  }

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

  {

  CCMenu*menu = (CCMenu*) [self getChildByTag:1025];

  menu.isTouchEnabled = YES;

  }

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

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

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

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.