[转载]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…

相关文章
相关标签/搜索