- 点击事件核心类:
MouseManager
和TouchManager
。
MouseManager
负责收集相关事件,进行捕获阶段和目标阶段。
TouchManger
负责处理和分发事件,进行冒泡阶段。
- 捕获阶段:此阶段引擎会从stage开始递归检测stage及其子对象,直到找到命中的目标对象或者未命中任何对象;
- 目标阶段:找到命中的目标对象;
- 冒泡阶段:事件离开目标对象,按节点层级向上逐层通知,直到到达舞台的过程。</p>
- 事件是由Canvas(浏览器控件等)发起,在
MouseManager
中注册处理。
-
MouseManager
在监听到事件后,会将事件添加到队列中,在stage进入下一帧时(Stage._loop
),一次性处理完所有事件。
- 捕获、目标阶段核心逻辑:(从根节点开始,向子节点查找)
- 初始化Event。MouseManager维护一个唯一的Event对象,保留鼠标事件相关信息,如target、touchid等。
- 先判断sp是否有scrollRect,如果则scrollRect外,直接判定为没点到,返回false。
- 优先检测(
HitTestPrior
),并且没有点击到,则直接返回false。(width>0并且没有点击穿透的View,默认会被设置为HitTestPrior = true
)
- 命中检测逻辑(
hitTest
)
- 参数为被判断的sp和转换为相对与sp的鼠标坐标(通过
fromParentPoint
方法)
- 如果有scrollRect,则先偏移鼠标点击位置。
- 如果sp的
hitArea
字段不为空,则返回sp的hitArea.isHit
结果。
- 如果
mouseThrough
为true,则检测子对象的实际大小进行碰撞。否则就使用(0,0,width,height)的矩形检测点是否在矩形内。
- 倒叙遍历子对象,从外向内检测,检测到后,直接跳过内部检测。
- 将目标对象和Event对象传递给
TouchManager
。
- 冒泡阶段核心逻辑:(从子节点开始,向根节点查找)
- 获取或创建的点击信息,用于检测拖拽、双击等。
- 从当前sp开始,递归收集父节点,按顺序放入数组中,子节点在前,父节点在后。
- 按照数组属性,对节点发送相关事件,如果事件被阻断(
event.stopPropagation
),则直接跳过所有父节点。事件的currentTarget为最先被点击的sp。
- 鼠标事件触发后,参数默认为
MouseManager
中的event对象。如果监听事件时,自己设置参数,则会在自定义参数数组后,添加event事件。
else if (args) result = method.apply(caller, args.concat(data));
MouseManager:
private function check(sp:Sprite, mouseX:Number, mouseY:Number, callBack:Function):Boolean {
this._point.setTo(mouseX, mouseY);
sp.fromParentPoint(this._point);
mouseX = this._point.x;
mouseY = this._point.y;
//如果有裁剪,则先判断是否在裁剪范围内
var scrollRect:Rectangle = sp.scrollRect;
if (scrollRect) {
_rect.setTo(scrollRect.x, scrollRect.y, scrollRect.width, scrollRect.height);
if (!_rect.contains(mouseX, mouseY)) return false;
}
//先判定子对象是否命中
if (!disableMouseEvent) {
//优先判断父对象
//默认情况下,hitTestPrior=mouseThrough=false,也就是优先check子对象
//$NEXTBIG:下个重大版本将sp.mouseThrough从此逻辑中去除,从而使得sp.mouseThrough只负责目标对象的穿透
if (sp.hitTestPrior && !sp.mouseThrough && !hitTest(sp, mouseX, mouseY)) {
return false;
}
for (var i:int = sp._childs.length - 1; i > -1; i--) { //倒叙遍历,从外向内检测,如果检测到则跳过内部检测
var child:Sprite = sp._childs[i];
//只有接受交互事件的,才进行处理
if (!child.destroyed && child.mouseEnabled && child.visible) {
if (check(child, mouseX, mouseY, callBack)) return true;
}
}
}
//避免重复进行碰撞检测,考虑了判断条件的命中率。
var isHit:Boolean = (sp.hitTestPrior && !sp.mouseThrough && !disableMouseEvent) ? true : hitTest(sp, mouseX, mouseY);
if (isHit) {
_target = sp;
callBack.call(this, sp);
} else if (callBack === onMouseUp && sp === _stage) {
//如果stage外mouseUP
_target = _stage;
callBack.call(this, _target);
}
return isHit;
}
private function hitTest(sp:Sprite, mouseX:Number, mouseY:Number):Boolean {
var isHit:Boolean = false;
if (sp.scrollRect) {
mouseX -= sp.scrollRect.x;
mouseY -= sp.scrollRect.y;
}
if (sp.hitArea is HitArea) {
return sp.hitArea.isHit(mouseX, mouseY);
}
if (sp.width > 0 && sp.height > 0 || sp.mouseThrough || sp.hitArea) {
//判断是否在矩形区域内
if (!sp.mouseThrough) {
var hitRect:Rectangle = this._rect;
if (sp.hitArea) hitRect = sp.hitArea;
else hitRect.setTo(0, 0, sp.width, sp.height); //坐标已转换为本地坐标系
isHit = hitRect.contains(mouseX, mouseY);
} else {
//如果可穿透,则根据子对象实际大小进行碰撞
isHit = sp.getGraphicBounds().contains(mouseX, mouseY);
}
}
return isHit;
}
/**
* 执行事件处理。
*/
public function runEvent():void {
var len:int = _eventList.length;
if (!len) return;
var _this:MouseManager = this;
var i:int = 0,j:int,n:int,touch:*;
while (i < len) {
var evt:* = _eventList[i];
if (evt.type !== 'mousemove') _prePoint.x = _prePoint.y = -1000000;
switch (evt.type) {
case 'mousedown':
_touchIDs[0] = _id++;
if (!_isTouchRespond) {
_this._isLeftMouse = evt.button === 0;
_this.initEvent(evt);
_this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseDown);
} else
_isTouchRespond = false;
break;
case 'mouseup':
_this._isLeftMouse = evt.button === 0;
_this.initEvent(evt);
_this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseUp);
break;
case 'mousemove':
if ((Math.abs(_prePoint.x - evt.clientX) + Math.abs(_prePoint.y - evt.clientY)) >= mouseMoveAccuracy) {
_prePoint.x = evt.clientX;
_prePoint.y = evt.clientY;
_this.initEvent(evt);
_this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseMove);
// _this.checkMouseOut();
}
break;
case "touchstart":
_isTouchRespond = true;
_this._isLeftMouse = true;
var touches:Array = evt.changedTouches;
for (j = 0, n = touches.length; j < n; j++) {
touch = touches[j];
//是否禁用多点触控
if (multiTouchEnabled || isNaN(_curTouchID)) {
_curTouchID = touch.identifier;
//200次点击清理一下id资源
if (_id % 200 === 0) _touchIDs = {};
_touchIDs[touch.identifier] = _id++;
_this.initEvent(touch, evt);
_this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseDown);
}
}
break;
case "touchend":
case "touchcancel":
_isTouchRespond = true;
_this._isLeftMouse = true;
var touchends:Array = evt.changedTouches;
for (j = 0, n = touchends.length; j < n; j++) {
touch = touchends[j];
//是否禁用多点触控
if (multiTouchEnabled || touch.identifier == _curTouchID) {
_curTouchID = NaN;
_this.initEvent(touch, evt);
var isChecked:Boolean;
isChecked = _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseUp);
if (!isChecked)
{
_this.onMouseUp(null);
}
}
}
break;
case "touchmove":
var touchemoves:Array = evt.changedTouches;
for (j = 0, n = touchemoves.length; j < n; j++) {
touch = touchemoves[j];
//是否禁用多点触控
if (multiTouchEnabled || touch.identifier == _curTouchID) {
_this.initEvent(touch, evt);
_this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseMove);
}
}
break;
case "wheel":
case "mousewheel":
case "DOMMouseScroll":
_this.checkMouseWheel(evt);
break;
case "mouseout":
//_this._stage.event(Event.MOUSE_OUT, _this._event.setTo(Event.MOUSE_OUT, _this._stage, _this._stage));
TouchManager.I.stageMouseOut();
break;
case "mouseover":
_this._stage.event(Event.MOUSE_OVER, _this._event.setTo(Event.MOUSE_OVER, _this._stage, _this._stage));
break;
}
i++;
}
_eventList.length = 0;
}
}
TouchManager
/**
* 派发事件。
* @param eles 对象列表。
* @param type 事件类型。
* @param touchID (可选)touchID,默认为0。
*/
private function sendEvents(eles:Array, type:String, touchID:int = 0):void {
var i:int, len:int;
len = eles.length;
_event._stoped = false;
var _target:*;
_target = eles[0];
var tE:Sprite;
for (i = 0; i < len; i++) {
tE = eles[i];
if (tE.destroyed) return;
tE.event(type, _event.setTo(type, tE, _target));
if (_event._stoped)
break;
}
}
/**
* 获取对象列表。
* @param start 起始节点。
* @param end 结束节点。
* @param rst 返回值。如果此值不为空,则将其赋值为计算结果,从而避免创建新数组;如果此值为空,则创建新数组返回。
* @return Array 返回节点列表。
*/
private function getEles(start:Node, end:Node = null, rst:Array = null):Array {
if (!rst) {
rst = [];
} else {
rst.length = 0;
}
while (start && start != end) {
rst.push(start);
start = start.parent;
}
return rst;
}