1.简介
最近在做个小的示例项目,确定后打算用cocos2dx框架来做UI部分,毕竟它易于使用还跨平台,像我这样几乎完全没有cocos2dx基础的童鞋,也能快速上手。在开发过程中,我想像在桌面应用中那样,弹出一个窗口并给出一些文本提示。无奈好像cocos2dx并没有给出现成可用的东西,只能自己琢磨实现了。考虑过后,打算通过Layer类来构建一个子类,然后在Scene中去显示该Layer,从而模拟出“窗口”的效果。我查阅了一些资料发现,网上有很多类似的实现代码,但他们大多已经过时,因这两年cocos2dx的版本变化好像挺大。
2.问题描述
按照上述的思路构建子类测试后发现,监听的事件不会被“阻隔”,例如:我们在Scene中监听了鼠标和键盘,然后在Scene中去添加了该Layer,并在该Layer中也监听了鼠标点击事件,当我们在该Layer中点击鼠标时,Scene中也会触发该事件。这就导致了一些奇怪的现象,和预期不符合。本文将试着简介如何用一种足够简单的方法,构建这样一个“模态对话框类”并解决包含事件不“阻隔”在内的几乎所有问题。
在开始前,先简要说明一下环境。本次实验编写于Linux平台,并提供了源码的Github地址【Download Source】。因实验于PC平台,我在示例所监听的都是鼠标和键盘事件,对于移动平台的触摸事件未做任何测试,但理论上也是可行的,具体请读者自测和变更,以适应特定的场景。
#ifndef MYGAME_POPUPLAYER_H
#define MYGAME_POPUPLAYER_H
#include "cocos2d.h"
USING_NS_CC;
class PopupLayer:public cocos2d::Layer
{
public:
PopupLayer();
virtual ~PopupLayer();
private:
std::string backgroundImage;
EventListenerMouse *listenerMouse;
public:
virtual bool init() override;
CREATE_FUNC(PopupLayer);
static PopupLayer* create(const std::string backgroundImage);
bool setBackgroundImage(const std::string &backgroundImage);
void okMenuItemCallback(Ref *pSender);
void cancelMenuItemCallback(Ref *pSender);
/** * Event callback that is invoked every time when Node enters the 'stage'. * If the Node enters the 'stage' with a transition,this event is called when the transition starts. * During onEnter you can't access a "sister/brother" node. * If you override onEnter,you shall call its parent's one,e.g.,Node::onEnter(). * @lua NA */
virtual void onEnter() override;
/** * Event callback that is invoked every time the Node leaves the 'stage'. * If the Node leaves the 'stage' with a transition,this event is called when the transition finishes. * During onExit you can't access a sibling node. * If you override onExit,Node::onExit(). * @lua NA */
virtual void onExit() override;
};
#endif //MYGAME_POPUPLAYER_H
首先 PopupLayer继承自Layer类,你也可以继承自其他Layer类,如LayerColor,这样你可以获得不同的特性。这里仅仅定义了两个数据成员,一个std::string用于存储整个“窗口”的背景图文件名,一个EventListenerMouse指针存储鼠标监听器,稍后可以看到是如何使用它们的。最后,来看看添加了哪些成员方法。首先,我重写了基类的init()方法,如果你对cocos2dx不陌生,那么你应该知道在该方法中要做哪些事情。其次,我使用 CREATE_FUNC 宏来构建了一个默认的静态create()方法,这基本上也是cocos2dx的传统。为了进一步满足需求,我还提供了静态create()方法的一个重载,它将包含一个参数用于指定背景资源名。setBackgroundImage()方法可以用来设置和更改背景资源。okMenuItemCallback()和cancelMenuItemCallback()是“模态对话框”上两个按钮的回调方法。最后我还重写了基类的onEnter()和onExit()方法,这两个方法至关重要,稍后你将看到它们是如何相互配合完成整个功能的。
cocos2dx使用一个类对象前,总是先调用它的静态create()方法来创建该类的一个实例,所以我们先从它开始介绍。
PopupLayer* PopupLayer::create(const std::string backgroundImage)
{
PopupLayer *pl = new(std::nothrow) PopupLayer();
if (pl && pl->setBackgroundImage(backgroundImage) && pl->init())
{
pl->autorelease();
return pl;
}
else
{
delete pl;
pl = nullptr;
return nullptr;
}
}
可以看到,我们只增加了setBackgroundImage()方法的调用。接着就是init()方法了。
bool PopupLayer::init()
{
// 1. super init first
if ( !Layer::init() )
{
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
this->setAnchorPoint(Vec2::ZERO);
// add "HelloWorld" splash screen"
auto sprite = Sprite::create(backgroundImage);
// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2,visibleSize.height/2));
// add the sprite as a child to this layer
this->addChild(sprite,1);
auto label = Label::createWithTTF("Are u sure exit?","fonts/Marker Felt.ttf",24);
label->setPosition(Vec2(visibleSize.width / 2,visibleSize.height / 2 + 50));
label->setColor(Color3B(255,0,0));
this->addChild(label,1);
auto okMenuItem = MenuItemFont::create("OK",CC_CALLBACK_1(PopupLayer::okMenuItemCallback,this));
okMenuItem->setPosition(Vec2(visibleSize.width / 2 - 100,visibleSize.height / 2 - 50));
okMenuItem->setColor(Color3B(255,0));
auto cancelMenuItem = MenuItemFont::create("Cancel",CC_CALLBACK_1(PopupLayer::cancelMenuItemCallback,this));
cancelMenuItem->setPosition(Vec2(visibleSize.width / 2 + 100,visibleSize.height / 2 - 50));
cancelMenuItem->setColor(Color3B(255,0));
auto pMenu = Menu::create(okMenuItem,cancelMenuItem,NULL);
pMenu->setPosition(Vec2::ZERO);
this->addChild(pMenu,1);
return true;
}
init()方法也足够简单,创建了一个sprite来作为背景,一个label来显示一些文本信息和两个菜单按钮,并设置了这两个菜单按钮的回调方法。
最后是onEnter()方法和onExit()方法,这是两个重点。onEnter()方法是在该“模态对话框”Layer显示时会被调用。相反,onExit()方法会在它消失时被调用。这两个机制可以用来做一些事情。
void PopupLayer::onEnter()
{
Layer::onEnter();
if (listenerMouse == nullptr)
{
listenerMouse = EventListenerMouse::create();
listenerMouse->onMouseDown = [](EventMouse* event) {
};
}
if (listenerMouse)
{
_eventDispatcher->pauseEventListenersForTarget(this->getParent(),true);
_eventDispatcher->addEventListenerWithSceneGraPHPriority(listenerMouse,this);
}
}
void PopupLayer::onExit()
{
Layer::onExit();
if (listenerMouse)
{
_eventDispatcher->removeEventListener(listenerMouse);
_eventDispatcher->resumeEventListenersForTarget(this->getParent(),true);
}
}
在onEnter()方法中,我们先创建一个鼠标监听对象,并设置了它的回调函数,只不过回调函数为空,什么也不做。然后通过eventDispatcher对象的pauseEventListenersForTarget()方法来暂停“模态对话框”父级窗口的事件监听器,并把自己的监听器添加到事件分发器中。所谓的父级窗口指的是“模态对话框”的下层Layer。这里是“模态对话框”下层Layer屏蔽事件监听的关键所在,如果不屏蔽它,则在“模态对话框”上发生的监听事件也会传递给它的下层Layer。也有的文章说可以设置监听器的优先级来解决该问题,但我没能找到解决方法。
在onExit()方法中,我们来做一些清理还原工作,因为onExit()方法被调用,意味着“模态对话框”Layer即将消失,所以我们从事件分发器中删除它的鼠标监听器,并重新启用它下层Layer的事件监听器。这样便可以还原。
最后,来看看如何使用它。我通过cocos2dx创建了一个新工程,并对HelloWorld场景做了一些小的改动,以方便测试。下面是它的init()方法。
bool HelloWorld::init()
{
//////////////////////////////
// 1. super init first
if ( !Layer::init() )
{
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
/////////////////////////////
// 2. add a menu item with "X" image,which is clicked to quit the program
// you may modify it.
// add a "close" icon to exit the progress. it's an autorelease object
auto closeItem = MenuItemImage::create(
"CloseNormal.png","CloseSelected.png",CC_CALLBACK_1(HelloWorld::menuCloseCallback,this));
closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2,origin.y + closeItem->getContentSize().height/2));
// create menu,it's an autorelease object
auto menu = Menu::create(closeItem,NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu,1);
/////////////////////////////
// 3. add your codes below...
// add a label shows "Hello World"
// create and initialize a label
auto label = Label::createWithTTF("Hello World",24);
// position the label on the center of the screen
label->setPosition(Vec2(origin.x + visibleSize.width/2,origin.y + visibleSize.height - label->getContentSize().height));
// add the label as a child to this layer
this->addChild(label,1);
auto label1 = Label::createWithTTF("Press the Enter key or click the left mouse button.",12);
// position the label on the center of the screen
label1->setPosition(Vec2(origin.x + visibleSize.width/2,100));
// add the label as a child to this layer
this->addChild(label1,1);
// add "HelloWorld" splash screen"
auto sprite = Sprite::create("HelloWorld.png");
// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2 + origin.x,visibleSize.height/2 + origin.y));
// add the sprite as a child to this layer
this->addChild(sprite,0);
//添加鼠标事件侦听
auto listenerMouse = EventListenerMouse::create();
listenerMouse->setEnabled(true);
listenerMouse->onMouseDown = [&](EventMouse* event) {
int mouseBtn = event->getMouseButton();
if (0 == mouseBtn) {
auto sprite = Sprite::create("HelloWorld.png");
sprite->setPosition(event->getLocationInView().x,event->getLocationInView().y);
sprite->setScale(0.5f,0.5f);
this->addChild(sprite,1);
}
};
_eventDispatcher->addEventListenerWithSceneGraPHPriority(listenerMouse,this);
//添加键盘事件监听
auto listenerKeyboard = EventListenerKeyboard::create();
listenerKeyboard->setEnabled(true);
listenerKeyboard->onKeyPressed = [&](EventKeyboard::KeyCode keycode,Event* event){
if(EventKeyboard::KeyCode::KEY_ENTER == keycode){
auto pl = PopupLayer::create("background.png");
pl->setScale(0.5f,0.5f);
pl->setParent(this);
pl->setPosition(Vec2(visibleSize.width/2,visibleSize.height/2));
pl->setAnchorPoint(Vec2(0.5f,0.5f));
this->addChild(pl,2);
}
};
_eventDispatcher->addEventListenerWithSceneGraPHPriority(listenerKeyboard,this);
return true;
}
我在里面增加了一个label,用于显示一些提示信息。增加了鼠标左键的事件监听,并在点击的位置绘制一张图片。这样做可以很好的演示当“模态对话框”Layer打开后,是否有效的屏蔽了下层Layer的事件监听。最后,添加了键盘监听,当我们敲击Enter键后,“模态对话框”Layer就会显示出来。值得特别注意的是,我调用了这样一个方法
pl->setParent(this);
用于设置了“模态对话框”Layer的父级Node,这是基类给提供的一个方法,千万别忘了设置它,其他的都足够简单了。 好了,你可以试着获取完整的代码,并编译运行一下,希望能解决你的问题或者给你一些新的思路。最后,我不太了解cocos2dx框架,如果本文或者示例中有什么错误或者不规范的地方,请指正,谢谢。