cocosCreator 事件系统
概述:
DOM的输入事件通过CCInputManager转化成cocos的输入事件,由CCEventManager 分发给监听器。
监听器在通过回调函数(begin/move/end/cancel)告知事件派发对象(eventTarget)派发事件。
重要类:
event:事件信息对象,主要有触摸信息、鼠标信息、键盘信息等。
eventTarget: 是一个事件派发的对象,有注册,删除和派发等功能,等同于订阅-发布模式。
eventListener:是一个事件监听器的对象,主要用于创建触摸、鼠标、键盘等监听器。包含了类型、回调函数、优先级等。
eventManager:是一个管理监听器的类,主要是对监听器进行排序、分发等。
eventManager
属性:
了解属性可以很好的知道这个类都有做了那些操作,结合函数可以知道是监听器的存储方式,划分监听器的,对监听器都会做什么处理。
_listenersMap: {}, //key:listenerID value:_EventListenerVector
_priorityDirtyFlagMap: {},//用来存储所有脏数据标记位的事件监听器,key:listenerID value:boolean
_nodeListenersMap: {},//key:node._id,value:listeners[] 存储节点的所有事件监听器
_toAddedListeners: [],//存储将要添加的事件监听器
_toRemovedListeners: [],//存储将要删除的事件监听器
_dirtyListeners: {},//key:listenerID value: boolean
_inDispatch: 0,//是否正在处理事件分派标记。 0 表示没有正在处理分派事件
_isEnabled: false,//事件管理器是否开启
_currentTouch: null,//当前正在处理的触摸事件对象
_currentTouchListener: null,//当前正在处理的事件监听器对象
_internalCustomListenerIDs: [],//用来存储自定义事件监听器ID
实现功能描述:
- 该类是监听器的管理者,因事件监听器有多种类型(总共五个),因此会对每种类型进行管理。
- 因事件监听器有触发的先后,所以在对事件监听器管理的时候加入了优先级概念。分为固定优先级、场景图优先级(渲染顺序)。
所以每种事件监听器类型,都有下列存储结构是:
_listenersMap:
- _fixedListeners 用于存储固定优先级的监听器(自定义监听器的优先级)
- _sceneGraphListeners 用于存储场景图优先级的监听器(就是node的)
- gtOIndex 用于标记固定优先级小于大于0的界限索引值。因为场景图优先级默认等于0,自定义优先级可=0
事件监听器触发顺序如下:
- 自定义优先级 priority<0
- 场景图优先级 listener 触发
- 自定义优先级 priority>=0的listener 最后触发。(gtOIndex的索引等于排序后监听器的priority等于0时在数组中的索引。)
自定义优先级时,priority值越小,优先级越高,值越小,优先级越低。场景图优先级_localZOrder越大,优先级越高。反之优先级越低。
-
为了可以管理节点上所有的事件监听器,有一个以下数据结构
_nodeListenersMap:
该数据存储着节点的所有类型的监听器,方便对节点上的所有监听器进行选择性的暂停、恢复、删除等功能。
在分发事件监听器时有个很重要的排序过程,其目的为了确定事件监听器的触发先后顺序。具体过程如下:
与排序相关的几个属性:
- _priorityDirtyFlagMap:这个map映射存储的是事件监听器类型与脏标记的映射。key=ListenerID;value=dirtyFlag;
- _dirtyListeners:暂时记录了有新的场景图优先级的监听器添加,或者是恢复了被暂停的监听器。key=Listener; value=boolean;
因为每种监听器都有固定优先级和场景图优先级之分。如果有新的事件监听器添加或者恢复,都会影响到新监听器相同类型的map数据排序顺序。
所以在每次dispatch的之前,都会对脏数据进行更新,暂时标记那种类型的监听器脏了。在具体的分发之前,会依据标记进行重新排序后再分发。
排序完成之后,_priorityDirtyFlagMap的value值都会标记位非脏数据。
同时有个optimize 点,只在真正分发时,进行一次排序,并非在加入监听器或恢复监听器时排序。
另外需要注意的是,节点的zIndex值被修改后,不单单影响渲染顺序,同时也会影响监听器的分发优先级。(sortAllchildren)
监听器的排序代码解析
- 自定义优先级排序算法就是根据设置的priority进行升序排序。priority 数值越小,越早被分发。
- 以下是场景图优先级的降序排序算法,层级越高,越早触发,与自定义优先级相反。
其主要考虑的问题又如下几个:1,祖父孙关系。2,非同父关系。3,同父关系
/**
sort的compare函数。降序
compare(a,b){
if(a>b)return -1;
if(a<b)return 1;
return 0;
}
**/
_sortEventListenersOfSceneGraphPriorityDes: function (l1, l2) {
let node1 = l1._getSceneGraphPriority(),
node2 = l2._getSceneGraphPriority();
//此l1优先
if (!l2 || !node2 || !node2._activeInHierarchy || node2._parent === null)
return -1;
//l2 优先
else if (!l1 || !node1 || !node1._activeInHierarchy || node1._parent === null)
return 1;
/**
1,如果是祖孙关系节点,则层级较浅的节点a会在查询一圈之后与层级较深节点b同时汇合在自己初始位置
2,如果是非祖孙关系节点,与上述一致,层级较浅的节点a优先到达"祖父"节点为空,到达后从深层级节点初始位置开始最终循环d-s后,ab的祖父节点相同。
3,如果是同父节点 直接退出
循环总次数计算:node1循环次数为空次数等于a,node2循环次数为空次数等于b
const max = Math.max(a,b);
const min = Math.max(a,b);
第一次循环min次之后,其中一个节点开始从另一个节点的起始位置开始。接着在循环(max-min)次之后,
较深节点会再循环一个差值之后与层级浅的节点 有着共同的祖父节点。总循环次数等于 max。
**/
let p1 = node1, p2 = node2, ex = false;
while (p1._parent._id !== p2._parent._id) {
p1 = p1._parent._parent === null ? (ex = true) && node2 : p1._parent;
p2 = p2._parent._parent === null ? (ex = true) && node1 : p2._parent;
}
//node1 与node2 是祖父孙关系,
//同祖父孙关系,较浅的节点a会一圈之后在自己初始位置与较深节点b重合。
if (p1._id === p2._id) {
if (p1._id === node2._id)// p1不等于node1(l1),p1比较深 l1优先
return -1;
if (p1._id === node1._id)//p1等于node1,p1比较浅,l2优先
return 1;
}
//到这一步已经确定不是祖孙关系了,那就存在 同父或不同父
//1,ex=true;p1与p2 是不同父节点,直接对比节点_localZOrder。
//2,ex=false; 是同父节点,因为降序所以反向减
return ex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder;
}
这里需要注意的是:_localZOrder 与zIndex、siblingIndex 区别
- _localZOrder 是一个32位的数字,前16存储着zIndex节点顺序,后16位存储着子节点相对于在父节点下的索引位置。
- 渲染顺序由_localZOrder 决定,节点排序是比较_localZOrder值进行升序排序。
- _localZOrder 前16位存储的是zIndex,后16位存储的事siblingIndex(setSiblingIndex)。
- zIndex权重比siblingIndex大,如果zIndex 相同,渲染顺序由siblingIndex决定。反之如果修改了zIndex,再次修改siblingIndex毫无效果。
- 修改zIndex时,_localZOrder值是在下一帧才会改变,当前帧并不会改变。
在事件监听器触发的过程中,可能会添加新的监听器或者删除监听器。因此有了下列属性
_toAddedListeners:当在添加新的监听器时,遇到正在触发事件监听器时,新的监听器或暂存在该数组中。
_toRemovedListeners:当在添加新的监听器时,遇到正在触发事件监听器时,新的监听器或暂存在该数组中。
_inDispatch:正在分发时>1; 不在分发时=0;
什么时候这些暂存的数据与总数据合并呢?每帧的时候都会去检测是否有暂存数据存在,如果有则与总数据合并(即添加、删除)。
监听器的吞噬与不吞噬,监听的的吞噬情况由三种条件控制
- 事件的stopPropagation、stopPropagationImmediate 如果事件设置了停止派发,则监听器的不吞噬也会暂停,其他监听器也不会接受到信息。
- 节点的hitTest 返回值 与 监听器的 swallowTouches 值设置。
- swallowTouches=true;吞噬 只会分发到自身监听器其他监听器不会被分发。
- swallowTouches=false; 不吞噬 下一个监听器也会被分发 依次循环, 直到有一个监听器设置吞噬才不会往下传
参考代码:_dispatchEventToListeners,shouldStopPropagation