Canvas 內部元素新增事件處理
前言
canvas 沒有提供為其內部元素新增事件監聽的方法,因此如果要使 canvas 內的元素能夠響應事件,需要自己動手實現。實現方法也很簡單,首先獲得滑鼠在 canvas 上的座標,計算當前座標在哪些元素內部,然後對元素進行相應的操作。配合自定義事件,我們就可以實現為 canvas 內的元素新增事件監聽的效果。
自定義事件
為了實現javascript物件的自定義事件,我們可以建立一個管理事件的物件,該物件中包含一個內部物件(當作map使用,事件名作為屬性名,事件處理函式作為屬性值,因為可能有個多個事件處理函式,所以使用陣列儲存事件處理函式),儲存相關的事件。然後提供一個激發事件的函式,通過使用 call
(function () { cce.EventTarget = function () { this._listeners = {}; this.inBounds = false; }; cce.EventTarget.prototype = { constructor: cce.EventTarget, // 檢視某個事件是否有監聽 hasListener: function (type) { if (this._listeners.hasOwnProperty(type)) { return true; } else { return false; } }, // 為事件新增監聽函式 addListener: function (type, listener) { if (!this._listeners.hasOwnProperty(type)) { this._listeners[type] = []; } this._listeners[type].push(listener); cce.EventManager.addTarget(type, this); }, // 觸發事件 fire: function (type, event) { if (event == null || event.type == null) { return; } if (this._listeners[event.type] instanceof Array) { var listeners = this._listeners[event.type]; for (var i = 0, len = listeners.length; i < len; i++) { listeners[i].call(this, event); } } }, // 如果listener 為null,則清除當前事件下的全部事件監聽 removeListener: function (type, listener) { if (listener == null) { if (this._listeners.hasOwnProperty(type)) { this._listeners[type] = []; cce.EventManager.removeTarget(type, this); } } if (this._listeners[type] instanceof Array) { var listeners = this._listeners[type]; for (var i = 0, len = listeners.length; i < len; i++) { if (listeners[i] === listener) { listeners.splice(i, 1); if (listeners.length == 0) cce.EventManager.removeTarget(type, this); break; } } } } }; }());
在上面的程式碼中,EventManager
用來儲存所有綁定了事件監聽的物件,便於後面判斷滑鼠是否位於某個物件內部。如果一個自定義物件需要新增事件監聽,只需要繼承 EventTarget
。
有序陣列
在判斷觸發某個事件的元素時,需要遍歷所有綁定了該事件的元素,判斷滑鼠位置是否位於元素內部。為了減少不必要的比較,這裡使用了一個有序陣列,使用元素區域的最小 x 值作為比較值,按照升序排列。如果一個元素區域的最小 x 值大於滑鼠的 x 值,那麼就無需比較陣列中該元素後面的元素。具體實現可以看 SortArray.js
元素父類
這裡設計了一個抽象類,來作為所有元素物件的父類,該類繼承了 EventTarget
(function () {
// 抽象類,該類繼承了事件處理類,所有元素物件應該繼承這個類
// 為了實現物件比較,繼承該類時應該同時實現compareTo, comparePointX 以及 hasPoint 方法。
cce.DisplayObject = function () {
cce.EventTarget.call(this);
this.canvas = null;
this.context = null;
};
cce.DisplayObject.prototype = Object.create(cce.EventTarget.prototype);
cce.DisplayObject.prototype.constructor = cce.DisplayObject;
// 在有序陣列中會根據這個方法的返回結果將物件排序
cce.DisplayObject.prototype.compareTo = function (target) {
return null;
};
// 比較目標點的x值與當前區域的最小 x 值,結合有序陣列使用,如果 point 的 x 小於當前區域的最小 x 值,那麼有序陣列中剩餘
// 元素的最小 x 值也會大於目標點的 x 值,就可以停止比較。在事件判斷時首先使用該函式過濾一下。
cce.DisplayObject.prototype.comparePointX = function (point) {
return null;
};
// 判斷目標點是否在當前區域內
cce.DisplayObject.prototype.hasPoint = function (point) {
return false;
};
}());
事件判斷
以滑鼠事件為例,這裡我們實現了 mouseover
, mousemove
, mouseout
三種滑鼠事件。首先對 canvas 新增 mouseover
事件,當滑鼠在 canvas 上移動時,會時時對比當前滑鼠位置與綁定了上述三種事件的元素的位置,如果滿足了觸發條件就呼叫元素的 fire
方法觸發對應的事件。下面是示例程式碼:
_handleMouseMove: function (event, container) {
// 這裡傳入container 主要是為了使用 _windowToCanvas函式
var point = container._windowToCanvas(event.clientX, event.clientY);
// 獲得綁定了 mouseover, mousemove, mouseout 事件的元素物件
var array = cce.EventManager.getTargets("mouse");
if (array != null) {
array.search(point);
// 滑鼠所在的元素
var selectedElements = array.selectedElements;
// 滑鼠不在的元素
var unSelectedElements = array.unSelectedElements;
selectedElements.forEach(function (ele) {
if (ele.hasListener("mousemove")) {
var event = new cce.Event(point.x, point.y, "mousemove", ele);
ele.fire("mousemove", event);
}
// 之前不在區域內,現在在了,說明滑鼠進入了
if (!ele.inBounds) {
ele.inBounds = true;
if (ele.hasListener("mouseover")) {
var event = new cce.Event(point.x, point.y, "mouseover", ele);
ele.fire("mouseover", event);
}
}
});
unSelectedElements.forEach(function (ele) {
// 之前在區域內,現在不在了,說明滑鼠離開了
if (ele.inBounds) {
ele.inBounds = false;
if (ele.hasListener("mouseout")) {
var event = new cce.Event(point.x, point.y, "mouseout", ele);
ele.fire("mouseout", event);
}
}
});
}
}
其他
立即執行函式
諸如下面形式的函式稱之為立即執行函式。
(function() {
// code
}());
使用立即執行函式的好處就是它限定了變數的作用域,使在立即執行函式中定義變數不會汙染其他作用域,更加詳細的講解請看這裡
apply, call, bind
這三個函式的使用類似於java 反射中的 Method.invoke
,方法作為一個主體,將執行方法的物件作為引數傳入到方法裡。其中 apply 和 call 作用一樣,呼叫後都會立即執行,只是接受引數的形式不同。
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
而 bind 會返回對應函式,不會立即執行,便於以後呼叫。 看下面的例子:
function aa() {
console.log(111);
console.log(this);
}
var bb = aa.bind(Math);
bb();
更加詳細的講解請看這裡
addEventListener 傳參
如果給某個元素新增事件監聽時需要傳遞引數,可以使用下面的方法
var i = 1;
aa.addEventListener("click", function() {
bb(i);
}, false);
呼叫父類的建構函式
使用 call
即可
Child = function() {
Parent.call(this);
}
物件檢測
- 判斷物件為 null 或者 undefined
// `null == undefined` 為true
if (variable == null) {
// code
}
- 判斷物件是否有某個屬性
if(myObj.hasOwnProperty("<property name>")){
alert("yes, i have that property");
}
// 或者
if("<property name>" in myObj) {
alert("yes, i have that property");
}
isPointInPath
canvas中判斷點是否在某個路徑內部,可以用於多邊形的檢測。不過 isPointInPath
使用路徑是最後一次繪製的圖形,如果有多個圖形需要判斷,需要將前面的圖形路徑儲存下來,判斷時需要重新構造路徑,不過不需要繪製,如下面
this.context.save();
this.context.beginPath();
//console.log(this.points);
this.context.moveTo(this.points[0].x, this.points[0].y);
for (var i = 1; i < this.points.length; i++) {
this.context.lineTo(this.points[i].x, this.points[i].y);
}
if (this.context.isPointInPath(target.x, target.y)) {
isIn = true;
}
this.context.closePath();
this.context.restore();