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; }; }());

事件判斷

以鼠標事件為例,這里我們實現了 mouseovermousemovemouseout 三種鼠標事件。首先對 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();

參考文章:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM