IOS开发:Cocos2d触摸分发原理分析

作者:杏彩彩票app下载

IOS开发:Cocos2d触摸分发原理分析

   触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear。鉴于同学们只会用cocos2d的 CCTouchDispatcher 的 api 但并不知道工作原理,但了解触摸分发的过程是极为重要的。毕竟涉及到权限、两套协议等的各种分发。

  本文以cocos2d-iphone源代码为讲解。cocos2d-x 于此类似,就不过多赘述了。

  零、cocoaTouch的触摸

  在讲解cocos2d触摸协议之前,我觉得我有必要提一下CocoaTouch那四个方法。毕竟cocos2d的Touch Delegate 也是通过这里接入的。

图片 1

  - (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的接入。

图片 2

  众所周知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

图片 3

  在说分发之前,还要介绍下这个类的作用。

  简而言之呢,这个类就是用于存储你的向分发器注册协议时的参数们。

  类指针,类所拥有的那几个函数们,以及触摸权限。

  只不过在 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…

触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear。鉴于同学们只会用...

Cocos2d CCLayer中的touch

触摸事件
iOS中的事件:
在用户使用app过程中,会产生各种各样的事件。iOS中的事件可以分为3大类型

Cocos2d 作为一个开源的2D游戏引擎,最初是用python语言实现,mac app开发流行后,提供了一个Objective-C的版本。采用Cocos2d框架开发iphone游戏,极大提高了开发的速度。简单介绍参见百度百科 ,cocos2d官网 。

图片 4

Cocos2d 提供了两种touch处理方式,Standard Touch Delegate和 Targeted Touch Delegate方式(参见CCTouchDelegateProtocol.h中源代码),CCLayer默认是采用第一种方式(参见CCLayer的 registerWithTouchDispatcher方法)。

5C07CC01-886C-42A3-90BB-FD63CF77FFCF.png

在CCLayer子类中要能接收touch事件,首先需要激活touch支持,在init方法中设置isTouchEnabled值为YES。

view的触摸事件处理:
响应者对象:

  • Standard Touch Delegate(CCLayer默认采纳这种方式)

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。

Standard方法中用户需要重载四个基本的touch处理方法,如下:

UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。
UIResponder:

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

UIResponder内部提供了以下方法来处理事件:

当touch事件发生时,会调用该方法响应touch事件。如果是单点touch,则只需要调用 UITouch *touch = [touches anyObject],就可以获取touch对象;如果需要响应多点 touch,则需要调用[[event allTouches] allObjects]返回一个UITouch的NSArray对象,然后使用NSArray的objectAtIndex依次访问各个UITouch对 象。为了获取UITouch对象的坐标(假设该UITouch名称为touch),调用[touch locationInView: [ touch view]]会返回一个UIView相关的坐标viewPoint。

// 触摸事件

使用Cocos2d的新建应用程序向导创建一个新的cocos2d application时,在xxxAppDelegate类的applicationDidFinishLaunching方法中CCDirector 会将UIView转换为支持OpenGL ES的EAGLView。此时,我们还需要将前面获取的UIView中的viewPoint转换为EAGLView坐标,调用[[CCDirector sharedDirector] convertToGL: viewPoint]即可实现。

  • (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;

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

// 加速计事件

-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
-(void) ccTouchesCancelled:(NSSet*)touch withEvent:(UIEvent *)event;
这三个方法和ccTouchesBegan类似。

  • (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  • (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  • (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  • Targeted Touch Delegate方式

// 远程控制事件

在standard方式中的响应处理事件处理的都是NSSet,而 targeted方式只处理单个的UITouch对象,在多点触摸条件下,应该采纳standard方式。在使用targeted方式之前需要重写 CCLayer中的registerWithTouchDispatcher方法:

  • (void)remoteControlReceivedWithEvent:(UIEvent *)event;

//记得在头文件中导入“CCTouchDispatcher.h”

UIView的触摸事件处理:

-(void) registerWithTouchDispatcher { 
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}

UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件:

targeted方式中用户需要重载4个基本的处理方法,其中ccTouchBegan必须重写,其他三个是可选的。

/* 一根或者多根手指开始触摸view,系统会自动调用view的下面方法 */

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event; (必须实现)

  • (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

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

/* 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法) */

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

  • (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

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

/* 一根或者多根手指离开view,系统会自动调用view的下面方法 */

每次touch事件发生时,先调用ccTouchBegan方法,该方法对每个UITouch进行响应并返回一个BOOL值,若为YES,则后续的 ccTouchMoved、ccTouchEnabled和ccTouchCancelled才会接着响应。

  • (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  • 多点触摸支持

/* 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法 */

在xxxAppDelegate类的applicationDidFinishLaunching 方法中加入下面代码 [glView setMultipleTouchEnabled:YES];

  • (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

// 提示:touches中存放的都是UITouch对象

UITouch:

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。一根手指对应一个UITouch对象。

UITouch的作用:
保存着跟手指相关的信息,比如触摸的位置、时间、阶段。

当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。当手指离开屏幕时,系统会销毁相应的UITouch对象。

提示:iPhone开发中,要避免使用双击事件!
UITouch的属性:

/* 触摸产生时所处的窗口 */
@property(nonatomic,readonly,retain) UIWindow *window;

/* 触摸产生时所处的视图 */
@property(nonatomic,readonly,retain) UIView *view;

/* 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击 */
@property(nonatomic,readonly) NSUInteger tapCount;

/*记录了触摸事件产生或变化时的时间,单位是秒 */
@property(nonatomic,readonly) NSTimeInterval timestamp;

/* 当前触摸事件所处的状态 */
@property(nonatomic,readonly) UITouchPhase phase;

UITouch的方法:

/*

  • 返回值表示触摸在view上的位置
  • 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
  • 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
    */
  • (CGPoint)locationInView:(UIView *)view;

本文由杏彩发布,转载请注明来源

关键词: