版权声明:本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名elloop(包含链接)
前言
本文回顾了cocos2d-x 2.x中触摸事件的使用方法和注意事项,侧重于单点触摸事件。
一个非常简单的触摸事件响应示例
在cocos2d-x中Layer是直接继承了触摸代理的类,也就是说它“天生”就带有响应触摸事件的功能. 要想启动Layer的触摸响应功能,仅需要一句代码:setTouchEnabled(true)
下面以hello world示例为基础,添加一下触摸的功能, 非常的简单, 请看代码:
编译运行代码,并且在CCLayer::ccTouchesBegan
, CCLayer::ccTouchesMoved
, CCLayer::ccTouchesEnded
等函数内添加断点,当用鼠标点击或在Layer上面做拖拽的时候,断点就会触发。
重写ccTouchesBegan等函数,实现自定义触摸事件响应逻辑
调用Layer的setTouchEnabled(true)之后,Layer默认开启了标准触摸(多点触摸)
模式,触摸事件发生时,它的ccTouchesXXX()系列方法会被调用;
还有第二种模式,叫TargetedTouch(单点触摸)
模式,通过调用layer->setTouchMode(kCCTouchesOneByOne)
来切换为单点触摸,触摸发生时,Layer的ccTouchXXX()系列方法会被调用, 注意这里跟多点触摸的区别是没有复数的es.
我们对触摸事件感兴趣无非就是为了在发生触摸事件的时候,要自己做一些事情,这些事情就是写在响应函数里的。那么如何实现自定义的响应逻辑呢?
在Layer的ccTouchBegan函数里已经提示我们了,看下面:
相信大家自己也能想到,在HelloWorld的Layer里通过重写ccTouchBegan系列函数就能实现自定义触摸事件的响应逻辑了。
我不是Layer,能接收触摸事件吗?
能! 你只需要让自己成为可触摸的(Touchable).
Layer为什么能够响应触摸事件?
触摸事件发生后,为什么是ccTouchesBegan, ccTouchesMoved等函数被调用, 而不是其它函数?
触摸事件的调度过程是典型的观察者模式,和常见的消息分发系统一样,你要是对某个事件感兴趣,那么你就注册为该事件的观察者,待该事件发生,事件分发中心会通知你。你要遵循事件中心的规定,准备好接口,方便事件中心将事件分发给你。
在触摸事件分发过程中,事件中心是CCTouchDispatcher, 你
是类似Layer这种观察者(我们姑且命名它为Touchable), 要准备好的接口由CCTouchDelegate来定义。
-
第一步,你要接收触摸事件,首先要让自己成为Touchable,这可以通过继承CCTouchDelegate来实现
-
第二步, 你要成为触摸事件的观察者,需要把自己注册到事件中心,可通过CCTouchDispatcher::addStandardDelegate
或者CCTouchDispatcher::addTargetedDelegate
来实现注册过程. 这两个接口只接收Touchable类型的东西,这也是第一步的原因
-
第三步, 重写CCTouchDelegate里的接口,加入自己的触摸响应逻辑代码,准备好接收触摸事件. 使用完及时把自己从事件中心移除掉.
可以通过下面的示例代码来说明这一过程:
回答上面那两个问题:
-
Layer为什么能够响应触摸事件? : 因为它继承了CCTouchDelegate, 它是Touchable, 并且通过调用setTouchEnabled(true)把自己注册到了事件中心CCTouchDispatcher.(setTouchEnabled只是打开了一个开关,真正的注册过程发生在Layer成为running状态的时候,请查看Layer::onEnter和Layer::registerWithTouchDispatcher).
-
为什么是ccTouchesBegan等函数得到调用? 因为它们是事件中心CCTouchDispatcher规定的事件接收接口(查看CCTouchDispatcher::touches方法可以看到对Touchable的回调过程),它通过CCTouchDelegate来实现这一约束。
后面会有自己实现的Touchable类型例子,这里就不贴示例代码了。
触摸事件分发系统类图
在刚才描述的触摸事件分发过程中,开发者可见的类有:事件分发中心CCTouchDispatcher, 触摸响应代理CCTouchDelegate
要响应触摸事件就继承CCTouchDelegate, 并且在CCTouchDispatcher中注册自己即可。
其实,在触摸事件分发过程中还有一个类很关键,它对逻辑开发者是透明的,它负责触摸事件优先级和是否吞噬事件的控制。它就是CCTouchHandler
. 它有两个子类,分别是负责处理标准多点触摸的handler:CCStandardTouchHandler
和单点触摸handler:CCTargetedTouchHandler
.
CCTouchHandler就像一个适配器,它内部含有一个Touchable, 同时添加了优先级和触摸吞噬控制的功能。
下面的类图给出了触摸事件分发过程中三个主要类之间的关系:
这个过程的重点是CCTouchDispatcher, 它的内部保存两个触摸处理器队列,一个m_pStandardHandlers保存所有注册来的标准触摸处理器;另一个m_pTargetedHandlers保存单点触摸处理器。
CCTouchDispatcher通过遍历两个处理器队列来实现触摸事件分发,通过每个处理器的handler->getDelegate()来取得Touchable(CCTouchDelegate)对象,进而调用ccTouchesBegan等函数。
分发机制
在cocos2d-x 2.x版本中,触摸事件的分发机制是这样的:
1. 优先级不同的情况下,优先级高的代理会最先得到调度,优先级是一个整数,越小意味着优先级越高
2. 优先级相同的情况下,后注册的代理会先得到调度,与元素的显示层级没有关系
这两点可以从CCTouchDispatcher的添加代理函数里得到证明:
来看一下触摸代理的分发过程:
从上面这段代码可以看到,对于TargetedTouch单点触摸是按照线性顺序来遍历分发的,那么排在队列前面的自然就会先分发
再看一下处理器是怎么排队的:
可以看到优先级高的会排在前面,优先级相同的,后来者会排在前面。因此,不难总结出上面提到的两条结论。
陷阱!!你以为你小我就摸不到你?
请看下面一个小例子,看看它的结果会让你吃惊吗?
**HelloWorldScene.h: **
头文件很简单,在HelloWorldScene下面加了一个TouchableLayer类,用来接收触摸事件. 下面是实现文件:
**HelloWorldScene.cpp: **
接下来运行代码, 效果如下图所示:
现在请考虑:
-
点击红色方块,它会旋转吗?
-
点击屏幕右半部分的黑色区域,红色方块会旋转吗?
第一个问题,相信大家都能答对,它会旋转,因为开启了触摸。
第二个问题呢,我们知道TouchableLayer的contentSize是100 x 100, 那么点击右半部分,肯定已经超出了contentSize, 也就是点击在了TouchableLayer的外面,它还会响应吗?
看效果图,答案是还会旋转!这可能有些出乎我们的预料,因为刚接触触摸事件,很容易把这个小红方块看成一个GUI概念里的按钮,我点在它的外面,它怎么还会响应呢?我刚接触这个地方的时候也很差异,但是你要知道,cocos是个游戏引擎,并不是GUI框架,它主要关心图形的绘制渲染,比GUI框架更加“底层化”,如果要实现一个GUI按钮的效果,那也需要你自己来处理触摸事件,判断点击范围,就是需要自己包装出来一个按钮出来。看看CCMenu对触摸事件的处理你就会明白这一过程了。
总结一下,这个例子要说的就是:
触摸事件的响应是和Touchable对象的大小没关系的
,哪怕你只是一个像素点,只要你注册了触摸事件,那么我点击屏幕的任何位置你都会收到响应。(前提是这个触摸事件没有被高优先级处理器吞噬掉)
触摸综合实例
下面的小实例,演示了如何自定义响应触摸事件的类、触摸事件的分发顺序、单点触摸事件的吞噬.
如图所示,一个父Layer中包含了1个小的彩色方块layer,和3只狗狗。(上面两个按钮分别是回测试主页和退出)
Layer和三只狗都注册了触摸事件,注册的时间先后顺序是dog1, dog2, dog3, Layer. 其中dog1, dog2, dog3是从左至右的顺序编号的。
这四个触摸代理注册的优先级只有dog1是-1,拥有最高优先级,dog2, dog3, Layer都是0. 4个代理都指定了swallow为true, 也就是说如果有一个代理处理了一个触摸事件,那么其它优先级低的代理就不会收到这个触摸事件
按照上文所述的事件分发机制(优先级高的在前,相同优先级后注册的在前)可以知道, 它们4个的触摸响应顺序会是: dog1 > Layer > dog3 > dog2
例子中的触摸代码逻辑是,如果触摸点在对应物体内部那么就处理该事件,否则不处理,即在ccTouchBegan()里面返回false.
-
彩色方块:
-
3只狗:
-
点击在其内部,会旋转90度,并在ccTouchBegan中返回true,代表处理了该事件,吞噬掉。
-
否则,点击在外部, 收到touchBegan的消息时,会进行抖动,并在ccTouchBegan里返回false,代表不处理该事件,也不会吞噬。
操作1:触摸空白处
没有点在任何一只狗内,也没有触摸在彩色方块内,因此事件一直向下转发,三只狗均触发touch,发生抖动.
操作2:触摸dog1
触摸在dog1, dog1旋转并吞噬事件,其它2只狗没触发touch.
操作3:触摸彩色方块
根据优先级,dog1先触发事件发生抖动且不吞噬,接着彩色方块触发触摸可以拖动,同时吞噬触摸,dog2, dog3没有触发touch
操作4:触摸dog3
dog1优先级最高总是被触发发生抖动且不吞噬,dog3触发在内部,旋转且吞噬触摸, dog2没有触发touch
操作5:触摸dog2
dog1优先级最高总是被触发发生抖动且不吞噬,dog3触发并抖动不吞噬,dog2触发touch, 旋转,吞噬。
操作6:把彩色方块放在dog1下面,并拖拽
因为触摸点在dog1内部,所以被dog1优先截获触摸并吞噬。彩色方块无法拖拽。dog2, dog3也不会触发touch.
操作6:把彩色方块放在dog2和dog3下面,并拖拽
彩色方块的优先级 > dog3 > dog2, 所以虽然彩色方块显示在dog2和dog3的下面,但是触摸响应是优先于2只狗的,因此可以实现拖拽,且吞噬了触摸事件,dog2和dog3既不会旋转也不会抖动。同时能看到dog1还是能够触发touch,因为它的优先级是最高的
触摸代理的优先级,是可以通过CCTouchDispatcher的setPriority(int nPriority, CCTouchDelegate *pDelegate)
接口来动态改变的, 在CCMenu::setHandlerPriority(int newPriority)
函数里能看到setPriority使用.
下面是代码的实现
TouchTestPage.h:
TouchTestPage.cpp:
注意:
-
对某一个代理来说,比如dog1:
-
ccTouchBegan() { return true;} 表示dog1对此次触摸感兴趣,会继续监听touchMoved, touchEnded等,即会继续回调到ccTouchMoved等.
-
ccTouchBegan() {return false;} 表示dog1对此次触摸不感兴趣, 不会继续回调到ccTouchMoved等函数。
-
对不同代理之间来说,比如dog1和dog3:
-
如果dog1在CCTouchDispatcher::addTargetedDelegate(…)里面指定了,swallow为true,且在dog1的ccTouchBegan返回了true,那么这个触摸事件就会被dog1吞噬掉,dog3及比dog3更低优先级的代理都不会收到触摸事件。
-
否则,如果dog1在addTargetedDelegate()没有指定swallow为true, 或者在ccTouchBegan里返回了false,那么触摸事件就会继续像下分发到dog3.
点击会变大的菜单按钮
点击缩放功能的菜单按钮放在下一篇说吧,这一篇已经很长了。
ccTouchCancelled什么时候会被调用?
在win32上没有试验出来,网上说是在非正常的触摸结束情况下会触发,比如在触摸过程中突然来电话。通常情况下,ccTouchCancelled函数里的处理方式和ccTouchEnded(正常触摸结束)保持一致就好。
总结
本文回顾了cocos中单点触摸的用法和触摸事件分发机制、优先级控制等内容. 关于多点触摸没怎么用,后面用了再总结。
在触摸事件当中,还有一些其他常见问题,比如如何定制自己的Menu实现点击缩放节省美术资源、点击穿透问题、如何在拖拽Scrollview上的按钮时候也能让Scrollview滚动等。这些问题将会在后面的文章中总结。
source codes
源代码仓库地址: cocos2d-x-cpp-demos-2.x, 是本人写的一个小型Demo框架,方便添加测试代码或者用来开发游戏,如果觉得有用请帮忙点个Star,谢谢
欢迎访问CSDN博客,与本站同步更新