- 點擊事件核心類:
MouseManager
和TouchManager
。
MouseManager
負責收集相關事件,進行捕獲階段和目標階段。
TouchManger
負責處理和分發事件,進行冒泡階段。- 捕獲階段:此階段引擎會從stage開始遞歸檢測stage及其子對象,直到找到命中的目標對象或者未命中任何對象;
- 目標階段:找到命中的目標對象;
- 冒泡階段:事件離開目標對象,按節點層級向上逐層通知,直到到達舞台的過程。
- 事件是由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)的矩形檢測點是否在矩形內。
- 參數為被判斷的sp和轉換為相對與sp的鼠標坐標(通過
- 倒敘遍歷子對象,從外向內檢測,檢測到后,直接跳過內部檢測。
- 將目標對象和Event對象傳遞給
TouchManager
。
- 冒泡階段核心邏輯:(從子節點開始,向根節點查找)
- 獲取或創建的點擊信息,用於檢測拖拽、雙擊等。
- 從當前sp開始,遞歸收集父節點,按順序放入數組中,子節點在前,父節點在后。
- 按照數組屬性,對節點發送相關事件,如果事件被阻斷(
event.stopPropagation
),則直接跳過所有父節點。事件的currentTarget為最先被點擊的sp。
- 鼠標事件觸發后,參數默認為
MouseManager
中的event對象。如果監聽事件時,自己設置參數,則會在自定義參數數組后,添加event事件。
else if (args) result = method.apply(caller, args.concat(data));
1 MouseManager: 2 private function check(sp:Sprite, mouseX:Number, mouseY:Number, callBack:Function):Boolean { 3 this._point.setTo(mouseX, mouseY); 4 sp.fromParentPoint(this._point); 5 mouseX = this._point.x; 6 mouseY = this._point.y; 7 8 //如果有裁剪,則先判斷是否在裁剪范圍內 9 var scrollRect:Rectangle = sp.scrollRect; 10 if (scrollRect) { 11 _rect.setTo(scrollRect.x, scrollRect.y, scrollRect.width, scrollRect.height); 12 if (!_rect.contains(mouseX, mouseY)) return false; 13 } 14 15 //先判定子對象是否命中 16 if (!disableMouseEvent) { 17 //優先判斷父對象 18 //默認情況下,hitTestPrior=mouseThrough=false,也就是優先check子對象 19 //$NEXTBIG:下個重大版本將sp.mouseThrough從此邏輯中去除,從而使得sp.mouseThrough只負責目標對象的穿透 20 if (sp.hitTestPrior && !sp.mouseThrough && !hitTest(sp, mouseX, mouseY)) { 21 return false; 22 } 23 for (var i:int = sp._childs.length - 1; i > -1; i--) { //倒敘遍歷,從外向內檢測,如果檢測到則跳過內部檢測 24 var child:Sprite = sp._childs[i]; 25 //只有接受交互事件的,才進行處理 26 if (!child.destroyed && child.mouseEnabled && child.visible) { 27 if (check(child, mouseX, mouseY, callBack)) return true; 28 } 29 } 30 } 31 32 //避免重復進行碰撞檢測,考慮了判斷條件的命中率。 33 var isHit:Boolean = (sp.hitTestPrior && !sp.mouseThrough && !disableMouseEvent) ? true : hitTest(sp, mouseX, mouseY); 34 35 if (isHit) { 36 _target = sp; 37 callBack.call(this, sp); 38 } else if (callBack === onMouseUp && sp === _stage) { 39 //如果stage外mouseUP 40 _target = _stage; 41 callBack.call(this, _target); 42 } 43 44 return isHit; 45 } 46 47 private function hitTest(sp:Sprite, mouseX:Number, mouseY:Number):Boolean { 48 var isHit:Boolean = false; 49 if (sp.scrollRect) { 50 mouseX -= sp.scrollRect.x; 51 mouseY -= sp.scrollRect.y; 52 } 53 if (sp.hitArea is HitArea) { 54 return sp.hitArea.isHit(mouseX, mouseY); 55 } 56 if (sp.width > 0 && sp.height > 0 || sp.mouseThrough || sp.hitArea) { 57 //判斷是否在矩形區域內 58 if (!sp.mouseThrough) { 59 var hitRect:Rectangle = this._rect; 60 if (sp.hitArea) hitRect = sp.hitArea; 61 else hitRect.setTo(0, 0, sp.width, sp.height); //坐標已轉換為本地坐標系 62 isHit = hitRect.contains(mouseX, mouseY); 63 } else { 64 //如果可穿透,則根據子對象實際大小進行碰撞 65 isHit = sp.getGraphicBounds().contains(mouseX, mouseY); 66 } 67 } 68 return isHit; 69 } 70 71 /** 72 * 執行事件處理。 73 */ 74 public function runEvent():void { 75 var len:int = _eventList.length; 76 if (!len) return; 77 78 var _this:MouseManager = this; 79 var i:int = 0,j:int,n:int,touch:*; 80 while (i < len) { 81 var evt:* = _eventList[i]; 82 83 if (evt.type !== 'mousemove') _prePoint.x = _prePoint.y = -1000000; 84 85 switch (evt.type) { 86 case 'mousedown': 87 _touchIDs[0] = _id++; 88 if (!_isTouchRespond) { 89 _this._isLeftMouse = evt.button === 0; 90 _this.initEvent(evt); 91 _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseDown); 92 } else 93 _isTouchRespond = false; 94 break; 95 case 'mouseup': 96 _this._isLeftMouse = evt.button === 0; 97 _this.initEvent(evt); 98 _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseUp); 99 break; 100 case 'mousemove': 101 if ((Math.abs(_prePoint.x - evt.clientX) + Math.abs(_prePoint.y - evt.clientY)) >= mouseMoveAccuracy) { 102 _prePoint.x = evt.clientX; 103 _prePoint.y = evt.clientY; 104 _this.initEvent(evt); 105 _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseMove); 106 // _this.checkMouseOut(); 107 } 108 break; 109 case "touchstart": 110 _isTouchRespond = true; 111 _this._isLeftMouse = true; 112 var touches:Array = evt.changedTouches; 113 for (j = 0, n = touches.length; j < n; j++) { 114 touch = touches[j]; 115 //是否禁用多點觸控 116 if (multiTouchEnabled || isNaN(_curTouchID)) { 117 _curTouchID = touch.identifier; 118 //200次點擊清理一下id資源 119 if (_id % 200 === 0) _touchIDs = {}; 120 _touchIDs[touch.identifier] = _id++; 121 _this.initEvent(touch, evt); 122 _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseDown); 123 } 124 } 125 126 break; 127 case "touchend": 128 case "touchcancel": 129 _isTouchRespond = true; 130 _this._isLeftMouse = true; 131 var touchends:Array = evt.changedTouches; 132 for (j = 0, n = touchends.length; j < n; j++) { 133 touch = touchends[j]; 134 //是否禁用多點觸控 135 if (multiTouchEnabled || touch.identifier == _curTouchID) { 136 _curTouchID = NaN; 137 _this.initEvent(touch, evt); 138 var isChecked:Boolean; 139 isChecked = _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseUp); 140 if (!isChecked) 141 { 142 _this.onMouseUp(null); 143 } 144 } 145 } 146 147 break; 148 case "touchmove": 149 var touchemoves:Array = evt.changedTouches; 150 for (j = 0, n = touchemoves.length; j < n; j++) { 151 touch = touchemoves[j]; 152 //是否禁用多點觸控 153 if (multiTouchEnabled || touch.identifier == _curTouchID) { 154 _this.initEvent(touch, evt); 155 _this.check(_this._stage, _this.mouseX, _this.mouseY, _this.onMouseMove); 156 } 157 } 158 break; 159 case "wheel": 160 case "mousewheel": 161 case "DOMMouseScroll": 162 _this.checkMouseWheel(evt); 163 break; 164 case "mouseout": 165 //_this._stage.event(Event.MOUSE_OUT, _this._event.setTo(Event.MOUSE_OUT, _this._stage, _this._stage)); 166 TouchManager.I.stageMouseOut(); 167 break; 168 case "mouseover": 169 _this._stage.event(Event.MOUSE_OVER, _this._event.setTo(Event.MOUSE_OVER, _this._stage, _this._stage)); 170 break; 171 } 172 i++; 173 } 174 _eventList.length = 0; 175 } 176 } 177 TouchManager 178 /** 179 * 派發事件。 180 * @param eles 對象列表。 181 * @param type 事件類型。 182 * @param touchID (可選)touchID,默認為0。 183 */ 184 private function sendEvents(eles:Array, type:String, touchID:int = 0):void { 185 var i:int, len:int; 186 len = eles.length; 187 _event._stoped = false; 188 var _target:*; 189 _target = eles[0]; 190 var tE:Sprite; 191 for (i = 0; i < len; i++) { 192 tE = eles[i]; 193 if (tE.destroyed) return; 194 tE.event(type, _event.setTo(type, tE, _target)); 195 if (_event._stoped) 196 break; 197 } 198 } 199 200 /** 201 * 獲取對象列表。 202 * @param start 起始節點。 203 * @param end 結束節點。 204 * @param rst 返回值。如果此值不為空,則將其賦值為計算結果,從而避免創建新數組;如果此值為空,則創建新數組返回。 205 * @return Array 返回節點列表。 206 */ 207 private function getEles(start:Node, end:Node = null, rst:Array = null):Array { 208 if (!rst) { 209 rst = []; 210 } else { 211 rst.length = 0; 212 } 213 while (start && start != end) { 214 rst.push(start); 215 start = start.parent; 216 } 217 return rst; 218 }