EaselJS ver 0.7で変更されたイベント処理を理解しよう

こんにちわ、ドラクエ8の誘惑に負けたkudoxです。少し時間が経ってしまいましたが、先日CreateJSの新バージョンがリリースされ、EaselJSのバージョンは0.7.xとなりました。EaselJS 0.7では、様々な機能追加や仕様変更が行われましたが、その中でもイベント関連の変更が目立ちます。そこで、EaselJS 0.7のイベント関連の変更点を簡単なサンプルと共に紹介したいと思います。

addEventListenerの第3引数 useCapture

EaselJS 0.7では、addEventListenerメソッドの第3引数でuseCaptureを指定できるようになりました。これにより、イベントが発生したオブジェクトの親オブジェクトでも処理をする場合、キャプチャフェーズとバブリングフェーズのどちらで処理をするか指定できるようになります。

まず、イベントの流れを図で見てみましょう。下図は、ShapeをContainerにaddChildし、ContainerをStageにaddChildした場合のイベントの流れを表しています。Shapeがクリックされた場合、1番のキャプチャフェーズではStageからContainerへとイベントが伝わり、2番のターゲットフェーズを経て、3番のバブリングフェーズでは、ContainerからStageへとイベントが伝わります。

EaselJS v0.7のイベントフロー

イベントが発生したShapeは、ターゲットフェーズで処理が行われますが、Shapeの親となるContainerやStageでは、キャプチャフェーズとバブリングフェーズのどちらで処理をするか指定することができます。これを指定するのがaddEventListenerの第3引数useCaptureです。

簡単なサンプルで確認してみましょう。左側のContainerではuseCaptureを省略してデフォルト値のfalseに、右側のContainerではuseCaptureにtrueを指定しています。クリックするとconsoleにEventオブジェクトのcurrentTarget.nameプロパティを出力するようにしていますが、useCaptureをtrueにした右側のContainerはキャプチャフェーズで処理が行われるため、イベントが発生したShapeよりも先に処理されていることが確認できます。

JavaScript

function eventInit() {
  _shapeL.addEventListener("click", clickHandler);
  _containerL.addEventListener("click", clickHandler);
  _shapeR.addEventListener("click", clickHandler);
  _containerR.addEventListener("click", clickHandler, true);
}

function clickHandler(evt) {
  console.log(evt.currentTarget.name);
}

イベントの伝播を止める Event.stopPropagation()

上の図で解説したようにイベントが親から子へ、子から親へ伝わることを「イベントの伝播」といいますが、EaselJS 0.7で新たに追加されたEventクラスのstopPropagationメソッドをコールするとイベントの伝播を止めることができます。

簡単なサンプルで確認してみましょう。サンプルでは、ShapeをContainerにaddChildし、それぞれにリスナー関数を追加しています。addEventListenerの第3引数を省略しているため、shapeClickHandler, containerClickHandlerの順で処理されるはずですが、shapeClickHandler内でEventオブジェクト.stopPropagation()をコールしているため、Containerにはイベントが伝わりません。

JavaScript

function eventInit() {
  _shape.addEventListener("click", shapeClickHandler);
  _container.addEventListener("click", containerClickHandler);
}

function shapeClickHandler(evt) {
  createjs.Tween.get(evt.currentTarget, {override:true})
  .to({scaleX:0.5, scaleY:0.5})
  .to({scaleX:1, scaleY:1}, 1000, createjs.Ease.elasticOut);
  evt.stopPropagation();
}

function containerClickHandler(evt) {
  window.alert(evt.currentTarget.name);
}

Stageのmouseleave, mouseenterイベント

EaselJS 0.7では、Stageクラスにmouseleaveとmouseenterイベントが追加されました。mouseleaveイベントはマウスポインタがStageから外側に出た時、mouseenterイベントはStage外から内側に入った時に発生します。また、Stageクラスには、tickstart, tickend, drawstart, drawendといったイベントも追加されているようです。

JavaScript

_stage.addEventListener("mouseleave", mouseLeaveHandler);
_stage.addEventListener("mouseenter", mouseEnterHandler);

rolloverとmouseoverの違い

EaselJS 0.7では、DisplayObjectクラスにrolloverとrolloutイベントが追加されました。以前のバージョンから存在するmouseover, mouseoutイベントとの違いを確認してみましょう。

下記のサンプルでは、円形のShapeと矩形のShapeを入れたContainerを左右に並べて、左側にrollover、右側にmouseoverでイベントリスナーを追加しています。実行してみると、rolloverではContainer内のShape間をマウスが行き来してもイベントが発生しないのに対して、mouseoverでは発生することがわかります。これは、rolloutとmouseoutでも同様です。

Flashをやられている方はご存知だと思いますが、この仕様はActionScript3.0と一緒ですね。ちなみにContainerクラスのmouseChildrenプロパティをfalseにするとmouseoverはrolloutと同様の動作になります。

JavaScript

_stage.enableMouseOver(FPS);
_containerL.addEventListener("rollover", hoverHandler);
_containerR.addEventListener("mouseover", hoverHandler);

pressmove, pressupを使ったドラッグ&ドロップ

これまでのバージョンでMouseEventに定義されていたmousemove, mouseupイベントがDepricated(将来廃止予定)となり、代わりにDisplayObjectにpressmove, pressupイベントが追加されました。pressmoveイベントはDisplayObject上でマウスボタンを押したまま動かした時、pressupイベントはDisplayObject上で押したマウスボタンを離した時に発生します。

下記のサンプルでは、pressmoveとpressupを使ってドラッグ&ドロップを実装しています。MouseEventクラスのコンストラクタに渡す引数も微妙に変更されているようなので、ご注意下さい。

JavaScript

function mouseDownHandler(mouseDownEvent) {
  _isDragging = true;
  var target = mouseDownEvent.target;
  var offsetX = target.x - mouseDownEvent.stageX
  var offsetY = target.y - mouseDownEvent.stageY;
  _lastPoint = new createjs.Point(target.x, target.y);
  function pressMoveHandler(pressMoveEvent) {
    _lastPoint = new createjs.Point(target.x, target.y);
    target.x = pressMoveEvent.stageX + offsetX;
    if (areaLimiter("x", target)) {
      offsetX = target.x - pressMoveEvent.stageX;
    }
    target.y = pressMoveEvent.stageY + offsetY;
    if (areaLimiter("y", target)) {
      offsetY = target.y - pressMoveEvent.stageY;
    }
  }
  function pressUpHandler(pressUpEvent) {
    target.removeEventListener("pressmove", pressMoveHandler);
    target.removeEventListener("pressup", pressUpHandler);
    _stage.removeEventListener("mouseleave", mouseLeaveHandler);
    _inertiaForce = new createjs.Point(target.x, target.y).subtract(_lastPoint);
    _lastPoint = null;
    _isDragging = false;
  }
  function mouseLeaveHandler(mouseLeaveEvent) {
    var mle = mouseLeaveEvent;
    var pue = new createjs.MouseEvent("pressup", true, false, mle.stageX, mle.stageY, mle.nativeEvent, mle.pointerID, mle.primary, mle.rawX, mle.rawY);
    target.dispatchEvent(pue);
  }
  target.addEventListener("pressmove", pressMoveHandler);
  target.addEventListener("pressup", pressUpHandler);
  _stage.addEventListener("mouseleave", mouseLeaveHandler);
}

EventDispatcher.on()とoff()

EaselJS 0.7では、0.6でDepricated(将来廃止予定)とされていたイベントハンドラが削除されました。その代わりにEventDispatcherクラスにonとoffメソッドが追加されています。EventDispatcher.onメソッドは、addEventListenerメソッドに比べて、いくつかの利点があります。

まず、第3引数でリスナー関数内でのthisスコープを指定することができます。そして第4引数のonceをtrueにするとリスナー関数を1回実行した後で自動でイベントリスナーを削除してくれます。また、第5引数でリスナー関数の第2引数で受け取る引数を指定することができます。

EventDispatcher.onメソッドは、戻り値でリスナーとなる関数を返します。これは、第2引数で指定した関数をcallメソッドで呼び出す無名関数です。戻り値の無名関数をEventDispatcher.offメソッドの第2引数に指定することでイベントリスナーを削除することができます。

EventDispatcher.onメソッドを使う上での注意点として、前述したように第2引数で指定した関数がリスナーとなるわけではなく、第2引数の関数を呼び出す無名関数がリスナーとなります。そのため、同じ関数を重複してリスナーとして登録できてしまう点には注意が必要です。

JavaScript

Star.prototype.eventInit = function() {
  var scope = this;
  var once = false;
  var data = {};
  var useCapture = false;
  var listener = this.on("click", this.clickHandler, scope, once, data, useCapture);
  data.listener = listener;
};

Star.prototype.clickHandler = function(evt, data) {
  console.log(this); // 出力:Star
  createjs.Tween.get(this, {override:true}).to({rotation:this.rotation + 72}, 1000, createjs.Ease.elasticOut);
  var useCapture = false;
  this.off("click", data.listener, useCapture);
  console.log(this.hasEventListener("click")); // 出力:false
};

イベントリスナーを削除してくれる Event.remove()

EaselJS 0.7のEventクラスには、removeメソッドが定義されています。これは、イベントオブジェクトからイベントリスナーを削除できる優れものです。下記のサンプルでは、リスナー関数内の処理でEventオブジェクトのremoveメソッドをコールして、イベントリスナーを削除しています。結果として、リスナー関数は1度しか実行されません。

しかし、removeの実行直後にhasEventListenerの戻り値を出力してみるとtrueが返されています。これは、Event.removeによってイベントリスナーが実際に削除されるのは、次にイベントが発生したタイミングであるからです。この仕様については、念のため頭に入れておきましょう。

JavaScript

Star.prototype.eventInit = function() {
  var scope = this;
  var once = false;
  var data = {};
  var useCapture = false;
  this.on("click", this.clickHandler, scope, once, data, useCapture);
};

Star.prototype.clickHandler = function(evt, data) {
  console.log(this); // 出力:Star
  createjs.Tween.get(this, {override:true}).to({rotation:this.rotation + 72}, 1000, createjs.Ease.elasticOut);
  evt.remove();
  console.log(this.hasEventListener("click")); // 出力:true
};