1 事件的工作机制
图1 传统事件系统如上图,模块A为事件触发者,模块B为事件响应者。A的实现依赖于模块B的实现,如果B的实现发生变化,A也可能需要作出相应调整。Cocos用订阅者模式将事件的触发者和响应者分开。触发者向一个公共的事件分发器发送一个事件消息,事件响应者向事件分发器订阅一个特定类型的消息来响应事件。以图1为例,B创建一个订阅者(ListenerB)并将此订阅者注册至事件分发器中,其中ListenerB附带了响应事件时需要执行的回调函数地址callBackFunc。当事件发生时,A使得事件发生器发出消息通知,以此触发B中的回调函数。
void GLView::handleTouchesBegin(int num,intptr_t ids[],float xs[],float ys[])
{
touchEvent._eventCode = EventTouch::EventCode::BEGAN;
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
void Widget::setTouchEnabled(bool enable)
{
if (enable == _touchEnabled)
{
return;
}
_touchEnabled = enable;
if (_touchEnabled)
{
_touchListener = EventListenerTouchOneByOne::create();
CC_SAFE_RETAIN(_touchListener);
_touchListener->setSwallowTouches(true);
_touchListener->onTouchBegan = CC_CALLBACK_2(Widget::onTouchBegan,this);
_touchListener->onTouchMoved = CC_CALLBACK_2(Widget::onTouchMoved,this);
_touchListener->onTouchEnded = CC_CALLBACK_2(Widget::onTouchEnded,this);
_touchListener->onTouchCancelled = CC_CALLBACK_2(Widget::onTouchCancelled,this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(_touchListener,this);
}
else
{
_eventDispatcher->removeEventListener(_touchListener);
CC_SAFE_RELEASE_NULL(_touchListener);
}
}
2 Cocos2dx中的事件分发器
2.1 基本组成
事件处理机制的组成部分包括:事件源、订阅者与分发者。事件源包含了该事件的类型Type与listenerID。订阅者包含了订阅者类型与listenerID,因此三者之间的关系是:分发器EventDispatch根据事件的类型找到对应的listenerID,进而找到所有该事件的订阅者。
2.2 注册订阅者
2.2.1 事件优先级
指定事件优先级有两个作用:1)让某些元素优先处理,并不再向后面的订阅者传递;2)控制元素间逻辑处理上的优先级。
void EventDispatcher::addEventListenerWithSceneGraphPriority(EventListener* listener,Node* node)
{
if (!listener->checkAvailable())
return;
listener->setAssociatedNode(node);
listener->setFixedPriority(0);
listener->setRegistered(true);
addEventListener(listener);
}
void EventDispatcher::addEventListenerWithFixedPriority(EventListener* listener,int fixedPriority)
{
if (!listener->checkAvailable())
return;
listener->setAssociatedNode(nullptr);
listener->setFixedPriority(fixedPriority);
listener->setRegistered(true);
listener->setPaused(false);
addEventListener(listener);
}
关联到具体UI的优先级指定方式很多时候要优于指定整数优先级的方式。后者需要开发者创建并关注一堆毫无意义的优先级枚举变量,这很有可能会导致某些问题,如:UI层级低的事件优先级数值比层级高的数值大,具体表现为:被遮挡的UI响应了触发事件而位于前面的UI却无任何反应。这肯定是不合理的。实际上,通过UI设置事件优先级的机制是在Cocos2dx 3.x之后引入的。
2.2.2 添加订阅者
上述关联代码需要关注的一个函数是addEventListener 。事件的分发是可以嵌套的,即可以在一个事件中触发另一个事件。_inDispatch记录了事件的嵌套数目,0表示没有事件需要分发。何时会发生事件的循环嵌套?举例说明:当按下A节点时,分发Touch事件,执行相关onTouchEvent 函数,该函数内实现了一个自定义事件,并再次调用事件分发函数,此时就产生了嵌套。
void EventDispatcher::addEventListener(EventListener* listener)
{
if (_inDispatch == 0)
{
forceAddEventListener(listener);
}
else
{
_toAddedListeners.push_back(listener);
}
listener->retain();
}
无嵌套时的添加过程 void EventDispatcher::forceAddEventListener(EventListener* listener)
{
EventListenerVector* listeners = nullptr;
EventListener::ListenerID listenerID = listener->getListenerID();
auto itr = _listenerMap.find(listenerID);
if (itr == _listenerMap.end())
{
listeners = new (std::nothrow) EventListenerVector();
_listenerMap.emplace(listenerID,listeners);
}
else
{
listeners = itr->second;
}
listeners->push_back(listener);
if (listener->getFixedPriority() == 0)
{
setDirty(listenerID,DirtyFlag::SCENE_GRAPH_PRIORITY);
auto node = listener->getAssociatedNode();
associateNodeAndEventListener(node,listener);
if (node->isRunning())
{
resumeEventListenersForTarget(node);
}
}
else
{
setDirty(listenerID,DirtyFlag::FIXED_PRIORITY);
}
} 该过程主要做了两件事:
将传入的订阅者添加至两个容器中:_listenerMap 与 _nodeListenersMap。前者以ListenerID 为key,后者以node为key。使用两个容器,以空间开销换时间检索效率;
标记订阅器:1) 将当前类型的订阅器做标记;2) 将node关联到的所有订阅器做标记。为何要做标记?这是用于加速订阅器的排序。订阅器的优先级随时会发生变动,为了保证事件分发能够按照正确顺序进行,事件分发时必须首先进行订阅器的排序。但为了避免频繁且重复的排序导致的性能问题,在订阅器发生变动时打上标记。排序时仅操作标记过的订阅者。具体排序实现后续会介绍。
void EventDispatcher::setDirty(const EventListener::ListenerID& listenerID,DirtyFlag flag)
{
auto iter = _priorityDirtyFlagMap.find(listenerID);
if (iter == _priorityDirtyFlagMap.end())
{
_priorityDirtyFlagMap.emplace(listenerID,flag);
}
else
{
int ret = (int)flag | (int)iter->second;
iter->second = (DirtyFlag) ret;
}
}
void EventDispatcher::setDirtyForNode(Node* node)
{
if (_nodeListenersMap.find(node) != _nodeListenersMap.end())
{
_dirtyNodes.insert(node);
}
const auto& children = node->getChildren();
for (const auto& child : children)
{
setDirtyForNode(child);
}
}
- 事件嵌套时的添加过程
当事件嵌套时,传入的订阅者不会立即被立即添加至相关容器中,而是先放置在待处理容器_toAddedListeners中。这些待处理的订阅者将在当前分发过程结束时加入,具体实现在后文的事件分发中描述。
2.3 事件分发
Touch事件是所有类型中最常用也是最复杂的一种事件,下文将以Touch事件为例详细剖析事件分发的核心过程。
2.3.1 事件触发源
touch事件的触发源在GLView中发生。
void GLView::handleTouchesBegin(int num,float ys[])
{
touchEvent._eventCode = EventTouch::EventCode::BEGAN;
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
void GLView::handleTouchesMove(int num,float ys[],float fs[],float ms[])
{
touchEvent._eventCode = EventTouch::EventCode::MOVED;
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
2.3.2 分发过程
事件分发的入口为dispatchEvent ,这一函数包含了事件分发的主要过程。我们将逐步研究函数内部细节实现。
void EventDispatcher::dispatchEvent(Event* event)
{
if (!_isEnabled)
return;
updateDirtyFlagForSceneGraph();
DispatchGuard guard(_inDispatch);
if (event->getType() == Event::Type::TOUCH)
{
dispatchTouchEvent(static_cast<EventTouch*>(event));
return;
}
}
updateDirtyFlagForSceneGraph 当关联的UI发生层级变化时,需要更新该UI节点对应的所有事件分发顺序。如在父控件上有A B两个子节点。起初A遮盖B,之后基于逻辑调整,B层级被调整并高过A,此时B事件响应等级也应当高于A。 void EventDispatcher::updateDirtyFlagForSceneGraph()
{
if (!_dirtyNodes.empty())
{
for (auto& node : _dirtyNodes)
{
auto iter = _nodeListenersMap.find(node);
if (iter != _nodeListenersMap.end())
{
for (auto& l : *iter->second)
{
setDirty(l->getListenerID(),DirtyFlag::SCENE_GRAPH_PRIORITY);
}
}
}
_dirtyNodes.clear();
}
} _dirtyNodes存放了一堆需要更新其订阅者的节点。这些节点什么时候会被加入至_dirtyNodes中呢?1) 节点的某个订阅者发生变化,如向该节点加入一个订阅者;2)节点的层级发生变化,实现过程如下。 void Node::setLocalZOrder(int z)
{
if (getLocalZOrder() == z)
return;
_setLocalZOrder(z);
if (_parent)
{
_parent->reorderChild(this,z);
}
_eventDispatcher->setDirtyForNode(this);
}
DispatchGuard 在函数内部创建一个DispatchGuard,该变量被分配在栈上,创建时_inDispatch嵌套数量加1,当前事件分发函数结束时变量自动析构,嵌套数减1。 class DispatchGuard
{
public:
DispatchGuard(int& count):_count(count)
{
++_count;
}
~DispatchGuard()
{
--_count;
}
private:
int& _count;
};
dispatchTouchEvent 该函数包含了Touch触摸事件分发的全部过程,包括:订阅者排序、将事件处理函数分发至订阅者以及更新订阅者。 void EventDispatcher::dispatchTouchEvent(EventTouch* event)
{
sortEventListeners(EventListenerTouchOneByOne::LISTENER_ID);
sortEventListeners(EventListenerTouchAllAtOnce::LISTENER_ID);
auto oneByOneListeners = getListeners(EventListenerTouchOneByOne::LISTENER_ID);
auto allAtOnceListeners = getListeners(EventListenerTouchAllAtOnce::LISTENER_ID);
if (nullptr == oneByOneListeners && nullptr == allAtOnceListeners)
return;
bool isNeedsMutableSet = (oneByOneListeners && allAtOnceListeners);
const std::vector<Touch*>& originalTouches = event->getTouches();
std::vector<Touch*> mutableTouches(originalTouches.size());
std::copy(originalTouches.begin(),originalTouches.end(),mutableTouches.begin());
if (oneByOneListeners)
{
auto mutableTouchesIter = mutableTouches.begin();
for (auto& touches : originalTouches)
{
bool isSwallowed = false;
auto onTouchEvent = [&](EventListener* l) -> bool {
EventListenerTouchOneByOne* listener = static_cast<EventListenerTouchOneByOne*>(l);
if (!listener->_isRegistered)
return false;
event->setCurrentTarget(listener->_node);
bool isClaimed = false;
std::vector<Touch*>::iterator removedIter;
EventTouch::EventCode eventCode = event->getEventCode();
if (eventCode == EventTouch::EventCode::BEGAN)
{
if (listener->onTouchBegan)
{
isClaimed = listener->onTouchBegan(touches,event);
if (isClaimed && listener->_isRegistered)
{
listener->_claimedTouches.push_back(touches);
}
}
}
else if (listener->_claimedTouches.size() > 0
&& ((removedIter = std::find(listener->_claimedTouches.begin(),listener->_claimedTouches.end(),touches)) != listener->_claimedTouches.end()))
{
isClaimed = true;
switch (eventCode)
{
case EventTouch::EventCode::MOVED:
if (listener->onTouchMoved)
{
listener->onTouchMoved(touches,event);
}
break;
case EventTouch::EventCode::ENDED:
if (listener->onTouchEnded)
{
listener->onTouchEnded(touches,event);
}
if (listener->_isRegistered)
{
listener->_claimedTouches.erase(removedIter);
}
break;
case EventTouch::EventCode::CANCELLED:
if (listener->onTouchCancelled)
{
listener->onTouchCancelled(touches,event);
}
if (listener->_isRegistered)
{
listener->_claimedTouches.erase(removedIter);
}
break;
default:
CCASSERT(false,"The eventcode is invalid.");
break;
}
}
if (event->isStopped())
{
updateListeners(event);
return true;
}
if (isClaimed && listener->_isRegistered && listener->_needSwallow)
{
if (isNeedsMutableSet)
{
mutableTouchesIter = mutableTouches.erase(mutableTouchesIter);
isSwallowed = true;
}
return true;
}
return false;
};
dispatchTouchEventToListeners(oneByOneListeners,onTouchEvent);
if (event->isStopped())
{
return;
}
if (!isSwallowed)
++mutableTouchesIter;
}
}
updateListeners(event);
} 1)订阅者排序 EventListenerTouchOneByOne 与 EventListenerTouchAllAtOnce的相关逻辑是分开处理的,因此排序方面也是独立进行。排序前,首先判断当前订阅者类型是否被记录在标记映射表内,如未标记则不进行任何操作;之后基于标记类型判断订阅者需要进行何种类型(数值指定优先级类型 与 节点赋予的优先级类型)的排序。 void EventDispatcher::sortEventListeners(const EventListener::ListenerID& listenerID)
{
DirtyFlag dirtyFlag = DirtyFlag::NONE;
auto dirtyIter = _priorityDirtyFlagMap.find(listenerID);
if (dirtyIter != _priorityDirtyFlagMap.end())
{
dirtyFlag = dirtyIter->second;
}
if (dirtyFlag != DirtyFlag::NONE)
{
dirtyIter->second = DirtyFlag::NONE;
if ((int)dirtyFlag & (int)DirtyFlag::FIXED_PRIORITY)
{
sortEventListenersOfFixedPriority(listenerID);
}
if ((int)dirtyFlag & (int)DirtyFlag::SCENE_GRAPH_PRIORITY)
{
auto rootNode = Director::getInstance()->getRunningScene();
if (rootNode)
{
sortEventListenersOfSceneGraphPriority(listenerID,rootNode);
}
else
{
dirtyIter->second = DirtyFlag::SCENE_GRAPH_PRIORITY;
}
}
}
} 下面来看如何对Node指定优先级类型的订阅者排序过程进行分析。该过程首先将需要排序的订阅者筛选出来。可以发现,检索操作十分频繁,检索得事件复杂度至多为O(n),而排序的最优效率最高为O(nlog(n)),因此排序相较于检索要更加耗时。为保证排序高效完成,在排序前要采用响应手段尽量将无需参与排序的元素剔除;完成筛选后,更新_nodePriorityMap,该表内存储了所有节点对应的优先级,作为后续排序的凭证。为保证节点优先级的实时性与有效性,每次进行排序时都需要从根节点深度遍历一次所有UI;最后基于最新的优先级排序订阅者。 void ventDispatcher::sortEventListenersOfSceneGraphPriority(const EventListener::ListenerID& listenerID,Node* rootNode)
{
auto listeners = getListeners(listenerID);
if (listeners == nullptr)
return;
auto sceneGraphListeners = listeners->getSceneGraphPriorityListeners();
if (sceneGraphListeners == nullptr)
return;
_nodePriorityIndex = 0;
_nodePriorityMap.clear();
visitTarget(rootNode,true);
std::stable_sort(sceneGraphListeners->begin(),sceneGraphListeners->end(),[this](const EventListener* l1,const EventListener* l2) {
return _nodePriorityMap[l1->getAssociatedNode()] > _nodePriorityMap[l2->getAssociatedNode()];
} );
} 遍历一遍UI树并获得最新的层级表_nodePriorityMap。为什么要做层级表更新?主要有两个原因:1)保证节点局部层级的准确性:一个父节点下包含一些层级小于0的子节点与一些层级大于等于0的子节点,绘制的时候先绘制层级小于0的,之后是父节点,之后是层级大于等于0的;2)保证节点全局层级的准确性:cocos2dx 3.x版本之后可指定任意节点的全局层级,绘制时会优先绘制全局层级高的。如何满足第一点?很简单,首先对相当节点所有子节点排序以保证层次有序,之后采用中序遍历遍历。巧妙之处在于:中序遍历的结果与节点绘制顺序完全一致。如何满足第二点?借助于_globalZOrderNodeMap映射表容器,以全局层次为key,node为value。没有设置全局层次的默认为0,在中序遍历是按照访问顺序被依次加入容器中,设置了全局层次的,也同理。在最后的节点优先级统计阶段,首先将_globalZOrderNodeMap依照key排序,然后从小到大遍历,将每个关联了订阅器的节点优先级加1。
void EventDispatcher::visitTarget(Node* node,bool isRootNode)
{
node->sortAllChildren();
int i = 0;
auto& children = node->getChildren();
auto childrenCount = children.size();
if(childrenCount > 0)
{
Node* child = nullptr;
for( ; i < childrenCount; i++ )
{
child = children.at(i);
if ( child && child->getLocalZOrder() < 0 )
visitTarget(child,false);
else
break;
}
if (_nodeListenersMap.find(node) != _nodeListenersMap.end())
{
_globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node);
}
for( ; i < childrenCount; i++ )
{
child = children.at(i);
if (child)
visitTarget(child,false);
}
}
else
{
if (_nodeListenersMap.find(node) != _nodeListenersMap.end())
{
_globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node);
}
}
if (isRootNode)
{
std::vector<float> globalZOrders;
globalZOrders.reserve(_globalZOrderNodeMap.size());
for (const auto& e : _globalZOrderNodeMap)
{
globalZOrders.push_back(e.first);
}
std::stable_sort(globalZOrders.begin(),globalZOrders.end(),[](const float a,const float b){
return a < b;
});
for (const auto& globalZ : globalZOrders)
{
for (const auto& n : _globalZOrderNodeMap[globalZ])
{
_nodePriorityMap[n] = ++_nodePriorityIndex;
}
}
_globalZOrderNodeMap.clear();
}
}
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|