【目标】添加ClickManager,管理触屏事件
【参考】
《cocos2dx学习之自定义你的CCSprite(二)监测长按和双击》: http://www.cnblogs.com/dcxing/archive/2013/01/19/2867643.html
一、为什么需要ClickManager
在cocos中,默认的 CCTouchDelegate 只能提供四种基本的事件:
1)ccTouchBegan 触屏开始,返回值为true才会继续触发后面的其他几种事件
2)ccTouchMoved 手指发生移动
3)ccTouchEnded 触屏事件正常结束,手指离开屏幕
4)ccTouchCancelled 触屏事件非正常结束(网上的说法是比如手指没正常离开时候来了个电话,未测试)
对于常见的操作来说,种类还是还少了一些,常见的点击、双击、长按等等手势动作都没有鉴别出来,因此需要强化。
强化的一种思路是对于需要接受触屏事件的类,例如CCLayer,进行一层封装,将触屏基本事件封装成更多的种类,不过由于许多CCSprite也需要通过继承CCTouchDelegate来获取触屏事件的处理权。本着用组合代替继承的思想,构造一个ClickManager来作为触屏事件的处理者,显然是更加优秀的解决方案。
那么,我们来考虑一下ClickManager的功能和实现思路:
1、最好是单实例,方便触屏事件的统一管理(不过这一点我这里没有实现,但实现起来并不麻烦,可能会在C/C++篇里面介绍)
2、要能获取基本的触屏事件,以便进行封装
3、要能够分辨长按、双击这种和时间相关的动作。
二、ClickManager的继承选择
有了ClickManager的实现目标,就大概会了解他需要拥有的资源,目前这里写的ClickManager只是一个简易版,只实现上面的2、3点,那么其继承如下:
class ClickManager : public cocos2d::CCObject, public cocos2d::CCTouchDelegate
下面分别大致的说明这两个父类:
1、CCObject:动态内存管理与时间调度的接受者
CCObject在cocos中的地位与java中的Object较为类似,可以理解为理解为所有类的基类,而将其他非继承与CCObject的类,也就是java语法中的interface。我们这里继承这个类,可以获得以下两个资源:
1)CREATE_FUNC:内存管理实在是有够麻烦,通过继承CCObject,可以将自己new出来的对象,加入到cocos的自动的内存管理中去,从而省去了内存管理的烦恼。
2)CCScheduler:这是另外一个非常重要的点。对于长按事件的识别,时间调度是必须的,而作为调度者的CCScheduler,其事件的分发目标只能是CCObject,所以如果不想写自己的CCScheduler,最好还是老老实实的继承CCObject。
2、CCTouchDelegate:触屏事件的接受者
触屏事件是由CCTouchDispatcher统一分发的,其回调接口只认CCTouchDelegate,如果想要接受触屏事件,需要通过
CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, priority, swallowEvent);
将自己加入到接收序列中,所以如果想要接收事件,就必须要继承于
CCTouchDelegate,实际上,这也基本已经是充分条件:即想要接收事件,在CCLayer打开setTouchEnabled 之外,就只需要加入这个队列,就可以通过覆写对应的 CCTouchDelegate 方法来获取响应事件了。
三、题外篇:ClickManager的回调选择
ClickManager终究并不是触屏事件的最终处理者,需要将封装后的事件回报给需要收到事件的对象。这个回报方式我考虑过两种,一种是JAVA式的,另一种是C++式的,下面分别来讨论这两种。
1)JAVA:面向接口编程
构建两个接口:ClickListener 和 LongTouchListener,需要接受事件的类继承需要的接口,并通过setListener来将该listener传递给 ClickManager,clickManager通过回调对应的方法来传递事件——这个方法很通用,很android,大致实现如下:
//接口定义
class ClickListener {
public:
virtual void onClick(cocos2d::CCTouch* touch, cocos2d::CCEvent* event) = 0;
};
class LongTouchListener {
public:
virtual void onLongTouch(cocos2d::CCTouch* touch, cocos2d::CCEvent* event) = 0;
};
…………
//ClickManager保存对应的变量
class ClickManager : public cocos2d::CCObject, public cocos2d::CCTouchDelegate {
private:
//处理者
ClickListener * clickHandler;
LongTouchListener * longTouchHandler;
//set方法
public:
void setClickHandler(ClickListener * handler);
void setLongTouchHandler(LongTouchListener * handler);
//回调举例:长按
void ClickManager::ccLongTouch(float dt) {
if ( isTouchCancelled || isTouchConsumed )
return;
isTouchConsumed = true;
if ( longTouchHandler != NULL )
longTouchHandler->onLongTouch(tmpTouch, tmpEvent);
}
2)C++:函数指针
这个是我最开头的想法,定义一个如下类型的函数指针:
typedef void (*HANDL_FUNC)(cocos2d::CCTouch* touch, cocos2d::CCEvent* event);
不过这种方法有个致命的问题:
他在类中并不适用,他不是面向对象的
。
原因在于,在类中,所有非static函数,都有一个隐藏的this指针,实际上这个指针是会作为一个隐藏的参数的,所以对于如下的两个函数:
class A {
void Foo(int x);
}
void FooOut(int x);
这两个函数的签名实际上并不一样,类中的Foo实际上还有一个隐藏的参数 A *。
所以,如果ClickManager要保留某个类的非静态函数指针,还需要知道这个类的信息,这就不合适了。所以最终这个方案被放弃了。
四、ClickManager的实现
前面讨论了很久ClickManager的架构,这里终于开始要动手写这个类了。
1、启动和结束
启动和结束时分别需要将自己加入或移除出消息接收队列,尤其移除这个动作必不可少,如果不做的话,会因为TouchDispatcher持有自己的一个引用导致无法自动析构(不过还是手动用CC_SAFE_RELEASE析构比较靠谱)。
void ClickManager::startReceiveTouchEvent(int priority, bool swallowEvent) {
CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, priority, swallowEvent);
}
void ClickManager::stopReceiveEvent() {
CCDirector::sharedDirector()->getTouchDispatcher()->removeDelegate(this);
}
2、获取 scheduler
CCObject本身没有 scheduler,需要从sharedDirector中获取一个,来方便后面进行各种跨时域的操作,这个动作可以在创建的时候做:
ClickManager::ClickManager() {
scheduler = CCDirector::sharedDirector()->getScheduler();
scheduler->retain();
…………
}
需要特别注意的是这里的 retain 动作
,其目的在于将
scheduler 的引用数加一,因为如果 scheduler 的引用数为0,是不能使用的,会在运行时报错。
3、长按的实现
思路就是在触屏开始时启动一个定时器,设定一个时间点,例如600ms,如果在这个期间,手指没有发生移动,也没有抬起,就说明发生了一次长按。
bool ClickManager::ccTouchBegan(CC
//一些标志位,在发生一次普通的点击或是长按被触发时,consumed会被置位,从而屏蔽对应的长按或点击事件。
//至于cancelled,则是在移动和取消的时候被置位
isTouchConsumed = false;
isTouchCancelled = false;
//取消以前规划的长按判断
scheduler->unscheduleSelector(schedule_selector(ClickManager::ccLongTouch), this);
//重新设置一个定时器
scheduler->scheduleSelector(schedule_selector(ClickManager::ccLongTouch), this, 0.6f, false, 0, 0.0f);
}
这里需要说明的是这个scheduleSelector的几个参数,第一个是函数指针,第二个是接受者,第三个是触发间隔,单位是秒,第四个是定时器是否要被暂停,主要是在退出的时候会被设置为true,第五个参数是触发的次数,我们一般使用 schdule宏的时候这个值被置为最大值 kCCRepeatForever,如果只需要被触发一次的话,只要写0就好,如果写1的话就会被触发两次,以此类推。最后一个参数是这个定时器延迟多久启动,有点类似 android 中 handler 的 sendMessageDelayed。
这里还做了一个防抖,思路就是手指移动的距离如果比较小,就忽略其移动。需要注意的是,每次传递进来的 CCTouch* 指针实际上都是同一个,所以要保留触屏开始时的位置,不能通过保留这个指针来实现,需要手动保存CCPoint
bool ClickManager::ccTouchBegan(CCTouch* touch, CCEvent* event) {
//保存开始的位置
beginPoint = touch->getLocation();
…………
return true;
}
void ClickManager::ccTouchMoved(cocos2d::CCTouch* touch, cocos2d::CCEvent* event) {
//允许用户手指产生一定的滑动
CCPoint pos = touch->getLocation();
int diff = pow(pos.x - beginPoint.x, 2) + pow(pos.y - beginPoint.y,2);
if ( diff < 5 )
return;
…………
}
如果不是抖动,而是发生了移动,或者干脆手指就抬起了,那么长按的计时器就需要被取消(当然通过标志位也可以实现),取消时间规划的方法如下:
scheduler->unscheduleSelector(schedule_selector(ClickManager::ccLongTouch), this);
需要注意的是,我们取得的 scheduler 是从 sharedDirector 里拿到的,实际上是一个单实例对象,
所以不要贸然使用 unscheduleAllSelectors
,会影响到所有时间规划。
4、点击的实现
实现思路如下:一次按下和抬起的事件对构成一次点击,不过这个期间不能发生移动,也不可以是长按。这个代码和上面比较接近,就不再贴了。