【HTML5】Canvas 內部元素添加事件處理

前言

canvas 沒有提供爲其內部元素添加事件監聽的方法,所以若是要使 canvas 內的元素可以響應事件,須要本身動手實現。實現方法也很簡單,首先得到鼠標在 canvas 上的座標,計算當前座標在哪些元素內部,而後對元素進行相應的操做。配合自定義事件,咱們就能夠實現爲 canvas 內的元素添加事件監聽的效果。javascript

源碼    演示
html

自定義事件

爲了實現javascript對象的自定義事件,咱們能夠建立一個管理事件的對象,該對象中包含一個內部對象(看成map使用,事件名做爲屬性名,事件處理函數做爲屬性值,由於可能有個多個事件處理函數,因此使用數組存儲事件處理函數),存儲相關的事件。而後提供一個激發事件的函數,經過使用 call 方法來調用以前綁定的函數。下面是代碼示例:java

(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 用來存儲全部綁定了事件監聽的對象,便於後面判斷鼠標是否位於某個對象內部。若是一個自定義對象須要添加事件監聽,只須要繼承 EventTargetgit

有序數組

在判斷觸發某個事件的元素時,須要遍歷全部綁定了該事件的元素,判斷鼠標位置是否位於元素內部。爲了減小沒必要要的比較,這裏使用了一個有序數組,使用元素區域的最小 x 值做爲比較值,按照升序排列。若是一個元素區域的最小 x 值大於鼠標的 x 值,那麼就無需比較數組中該元素後面的元素。具體實現能夠看 SortArray.jsgithub

元素父類

這裏設計了一個抽象類,來做爲全部元素對象的父類,該類繼承了 EventTarget,而且定義了三個函數,全部子類都應該實現這三個函數。 具體代碼以下所示:canvas

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

其餘

當即執行函數

諸以下面形式的函數稱之爲當即執行函數。app

(function() {
    // code
}());

使用當即執行函數的好處就是它限定了變量的做用域,使在當即執行函數中定義變量不會污染其餘做用域,更加詳細的講解請看這裏函數

apply, call, bind

這三個函數的使用相似於java 反射中的 Method.invoke,方法做爲一個主體,將執行方法的對象做爲參數傳入到方法裏。其中 apply 和 call 做用同樣,調用後都會當即執行,只是接受參數的形式不一樣。this

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

參考文章:

相關文章
相關標籤/搜索