【cocos2D-x学习】8.自己的触屏管理——添加ClickManager

【目标】添加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、点击的实现

      实现思路如下:一次按下和抬起的事件对构成一次点击,不过这个期间不能发生移动,也不可以是长按。这个代码和上面比较接近,就不再贴了。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值