之前写过了ETMutiMenu实现了多点触控的菜单类,突然想起来,就来写篇文章来分析下cocos2dx-3.0中的Menu

用法

我刚刚开始的时候,很笨的先创建好MenuItem*然后用Menu的某个静态方法来创建。
后来阅读源码之后,发现Menu是遍历所有子儿子的,所以只要创建一个空的Menu,之后addChild便是。
这里需要注意的是Menu的子儿子必须是MenuItem的子类,不然在运行过程中会崩溃。

路径

1
cocos/2d/menu-nodes/CCMenu.h
cocos/2d/menu-nodes/CCMenu.cpp

源码分析

先贴上头文件,删去了一些我认为不是很重要的类声明,想要自己仔细看看的,还是自己去看源码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#ifndef __CCMENU_H_
#define __CCMENU_H_

#include "CCMenuItem.h"
#include "CCLayer.h"
#include "CCVector.h"
#include "CCEventTouch.h"
#include "CCValue.h"

NS_CC_BEGIN

class CC_DLL Menu : public Layer
{
public:
/* Menu的状态 */
enum class State
{
WAITING,
TRACKING_TOUCH,
};

/* 开头都是一些静态类方法,用于创建菜单类 */
static Menu* create();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8)
/* 此处略去 */
#else
/** creates a Menu with MenuItem objects */
static Menu* create(MenuItem* item, ...) CC_REQUIRES_NULL_TERMINATION;
#endif

/* 此处略去,即重载的创建菜单类*/

/* 此处略去,主要是对菜单内菜单项的各种排列函数 */
/* 以后有空仔细看看,今天晚上就算了,明天还要起来洗衣服,真是忧伤=。= */

/* 开启关闭菜单 */
virtual bool isEnabled() const { return _enabled; }
virtual void setEnabled(bool value) { _enabled = value; };

/* 触摸时间回调 */
virtual bool onTouchBegan(Touch* touch, Event* event);
virtual void onTouchEnded(Touch* touch, Event* event);
virtual void onTouchCancelled(Touch* touch, Event* event);
virtual void onTouchMoved(Touch* touch, Event* event);

// overrides
virtual void removeChild(Node* child, bool cleanup) override;

/* 重载了addChild */
virtual void addChild(Node * child) override;
virtual void addChild(Node * child, int zOrder) override;
virtual void addChild(Node * child, int zOrder, int tag) override;

virtual void onEnter() override;
virtual void onExit() override;

/* 这两个暂时不知什么用 */
virtual void setOpacityModifyRGB(bool bValue) override {CC_UNUSED_PARAM(bValue);}
virtual bool isOpacityModifyRGB(void) const override { return false;}

/* Node */
virtual std::string getDescription() const override;

CC_CONSTRUCTOR_ACCESS: /* protected*/

Menu() : _selectedItem(nullptr) {}
virtual ~Menu();

bool init();

/* 在这里进行初始化,属性的设置,以及注册触摸事件 */
bool initWithArray(const Vector<MenuItem*>& arrayOfItems);

protected:
bool _enabled;

/* 关键的一个函数,来判断某个MenuItem是否被触摸到 */
MenuItem* getItemForTouch(Touch * touch);

/* 存储当前Menu的状态 */
State _state;

/* 存储当前被触摸的MenuItem */
/* 所以Menu就是支持单个触摸的 */
/* 你看看那个initWithArray,里面注册的触摸类型也都是TouchOneByOne */
/* 我之前写的那个ETMutiMenu的话,这里是一个数组 */
MenuItem *_selectedItem;

private:
CC_DISALLOW_COPY_AND_ASSIGN(Menu);
};

NS_CC_END

#endif//__CCMENU_H_

当然是先看看初始化的相关的函数initWithArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
bool Menu::initWithArray(const Vector<MenuItem*>& arrayOfItems)
{
if (Layer::init())
{
/* 开启触摸 */
_enabled = true;

/************************************************/
/* 此处略去,主要是描点,大小,位置等信息的设定 */
/************************************************/

/* 将列表中的MenuItem添加到Menu的子儿子中 */
/* 所以还是自己addChild来的好,还可以自定义每个MenuItem的z轴 */
int z=0;
for (auto& item : arrayOfItems)
{
this->addChild(item, z);
z++;
}

_selectedItem = nullptr;
_state = Menu::State::WAITING;

setCascadeColorEnabled(true);
setCascadeOpacityEnabled(true);

/* 注册一个单点触摸事件 */
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->setSwallowTouches(true);

/* 这里算是绑定回调函数 */
/* 这里是用到了c++11的特性 -- std::bind */
touchListener->onTouchBegan = CC_CALLBACK_2(Menu::onTouchBegan, this);
touchListener->onTouchMoved = CC_CALLBACK_2(Menu::onTouchMoved, this);
touchListener->onTouchEnded = CC_CALLBACK_2(Menu::onTouchEnded, this);
touchListener->onTouchCancelled = CC_CALLBACK_2(Menu::onTouchCancelled, this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

return true;
}
return false;
}

接着当然是看看触摸事件绑定的那四个函数怎么写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
bool Menu::onTouchBegan(Touch* touch, Event* event)
{
/* 若Menu状态不对,不可见,或是不可触摸,则直接返回false */
if (_state != Menu::State::WAITING || ! _visible || !_enabled) { return false; }

/* 查找所有直系祖先节点,是否不可见, 若不可见则直接返回false */
for (Node *c = this->_parent; c != nullptr; c = c->getParent())
{
if (c->isVisible() == false) { return false; }
}

/* 若获触摸到MenuItem则返回该按钮, 并触发该MenuItem的selected函数, 并进行下面的事件 */
/* 否则返回false */
_selectedItem = this->getItemForTouch(touch);
if (_selectedItem)
{
_state = Menu::State::TRACKING_TOUCH;
_selectedItem->selected();

return true;
}

return false;
}

void Menu::onTouchEnded(Touch* touch, Event* event)
{
CCASSERT(_state == Menu::State::TRACKING_TOUCH, "[Menu ccTouchEnded] -- invalid state");
this->retain();

/* 这里是松开按钮的时候 */
/* 调用MenuItem的unselected和activeate */
/* 若该MenuItem还绑定了回调函数,会在这个时候调用 */
if (_selectedItem)
{
_selectedItem->unselected();
_selectedItem->activate();
}

/* 到此一次触摸结束,Menu重回WAITING状态 */
_state = Menu::State::WAITING;
this->release();
}

/* 这个取消回调调用之后马上调用了onTouchEnded */
void Menu::onTouchCancelled(Touch* touch, Event* event)
{
CCASSERT(_state == Menu::State::TRACKING_TOUCH, "[Menu ccTouchCancelled] -- invalid state");
this->retain();

if (_selectedItem) { _selectedItem->unselected(); }

_state = Menu::State::WAITING;
this->release();
}

void Menu::onTouchMoved(Touch* touch, Event* event)
{
CCASSERT(_state == Menu::State::TRACKING_TOUCH, "[Menu ccTouchMoved] -- invalid state");

/* 获得当前触摸到的MenuItem */
MenuItem *currentItem = this->getItemForTouch(touch);

/* 这里的情况是,从一个MenuItem的开始一直按住,然后移动到另一个MenuItem */
if (currentItem != _selectedItem)
{
/* 这里是将原来的那个恢复原状,即未选择状态 */
if (_selectedItem) { _selectedItem->unselected(); }

/* 记录新按下的按钮,并将该按钮置为按下状态 */
_selectedItem = currentItem;
if (_selectedItem) { _selectedItem->selected(); }
}
}

上面讲到的一个很关键的函数getItemForTouch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MenuItem* Menu::getItemForTouch(Touch *touch)
{
Point touchLocation = touch->getLocation();

/* 这个函数的功能是,遍历所有可见,可触摸的子儿子 */
/* 检查是否有被触摸的MenuItem */
/* 若有,则返回该MenuItem */
/* 否则,返回nullptr */
if (!_children.empty())
{
for (auto iter = _children.crbegin(); iter != _children.crend(); ++iter)
{
/* 这里的强制转换,表示子儿子必须是MenuItem的子类 */
MenuItem* child = dynamic_cast<MenuItem*>(*iter);
/* 这里只检查可见,开启触摸的MenuItem */
if (child && child->isVisible() && child->isEnabled())
{
Point local = child->convertToNodeSpace(touchLocation);
Rect r = child->rect();
r.origin = Point::ZERO;

/* 若触摸点在按钮矩形boudingBox内的话,返回改按钮 */
if (r.containsPoint(local))
{
return child;
}
}
}
}

return nullptr;
}

本来想最开始讲的onEneteronExit现在放到了最后说,也是醉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 实际上onEnter没啥特别的 */
/* 本来是认为触摸函数的注册会放这里 */
void Menu::onEnter()
{
Layer::onEnter();
}

void Menu::onExit()
{
if (_state == Menu::State::TRACKING_TOUCH)
{
if (_selectedItem)
{
_selectedItem->unselected();
_selectedItem = nullptr;
}
_state = Menu::State::WAITING;
}

Layer::onExit();
}

void Menu::removeChild(Node* child, bool cleanup)
{
MenuItem *menuItem = dynamic_cast<MenuItem*>(child);
CCASSERT(menuItem != nullptr, "Menu only supports MenuItem objects as children");

if (JselectedItem == menuItem) { _selectedItem = nullptr; }

Node::removeChild(child, cleanup);
}

最后看到onExit会在Menu正在处理触摸状态下对以选中的MenuItem释放。
而且removeChild自然也是会对正在选中的MenuItem释放=。=

到此,Menu的分析已经结束了,下篇文章还是整理下NodeRef这些基类吧。


Ending

帅哥最近貌似也挺忙的,自己这几天也懈怠了,批评下自己,帅哥啊,求提醒啊,求监督啊。