ドラッグ&ドロップ

定番ですが、ドラッグ&ドロップのできるノードをJavaFXで実装してみました。

実装自体は簡単なので、後は使いやすいようなインターフェイスにすることが重要です。いつものごとく、サンプルとソースコードこちらから*1

使用例

こんな感じに使えます(各オプションは省略可能)。

        Draggable {
            content: [
                Rectangle { // ドラッグするノード
                    width: 50, height: 50
                    fill: Color.LIGHTYELLOW
                }// Rectangle
            ]
            axis: Axis.Y // Y方向のみ
            grid: [50, 50] // 50,50でグリッド
            // マウスを放したときのアクション
            // falseを返すと元の位置に戻る
            action: function(x, y) {
                println("cancel!");
                false;
            }
            showOriginal: true // 元のノードを表示する
            dragOpacity: 0.5 // ドラッグするノードの不透明度
            dragCursor: Cursor.HAND // カーソル
        }// Draggable

実装

public class Draggable extends CustomNode {
    public-init var content:Node[];
    public var axis: Axis;
    public var dragCursor: Cursor = Cursor.MOVE;
    public var dragOpacity: Number = 1.0;
    public var grid: Integer[] on replace {
        var size = sizeof(grid);
        if (size == 0) {
            gridX = gridY = 0;
        } else if (size == 1) {
            gridX = gridY = grid[0];
        } else {
            gridX = grid[0];
            gridY = grid[1];
        }
    }
    public var showOriginal:Boolean = false;
    public var action:function(newX:Number, newY:Number): Boolean = function(x, y) { true };
    var gridX:Integer;
    var gridY:Integer;
    var baseGroup: Group;
    var drag: Boolean = false;
    var dragX: Number = 0;
    var dragY: Number = 0;
    override function create():Node {
        Group {
            content: [
                baseGroup = Group {
                    content: content
                    opacity: bind if (not drag or showOriginal) 1.0 else 0.0
                    onMousePressed: function(e): Void {
                        drag = true;
                    }
                    onMouseDragged: function(e): Void {
                        if (axis != Axis.Y) dragX = SfUtil.adjust(e.dragX, gridX);
                        if (axis != Axis.X) dragY = SfUtil.adjust(e.dragY, gridY);
                    }
                    onMouseReleased: function(e): Void {
                        if (action(layoutX + dragX, layoutY + dragY)) {
                            drag = false;
                            layoutX += dragX;
                            layoutY += dragY;
                            dragX = dragY = 0;
                        } else {
                            revert();
                        }
                    }
                }// Group - baseGroup
                Group {
                    content: Duplicator.duplicate(baseGroup);
                    layoutX: bind baseGroup.layoutX, layoutY: bind baseGroup.layoutY
                    translateX: bind dragX, translateY: bind dragY
                    opacity: bind dragOpacity
                    visible: bind drag
                }// Group
            ]
            cursor: bind if(drag) dragCursor else Cursor.DEFAULT
        }// Group
    }
    function revert() {
        Timeline {
            keyFrames: [
                KeyFrame {
                    time: 0.25s
                    values: [ dragX => 0, dragY => 0]
                    action: function() { drag = false;}
                }
            ]
        }.play();
    }
}

以下、ポイントとなる箇所を解説。

まず、Draggableを使う場合はcontentにドラッグしたいノードを登録するわけですが、ちょっと見栄えを良くするために、元のノードを残してドラッグするようなエフェクトを使えるようにします。すると、元のノードを複製する必要があるわけですが、Duplicatorを使用するとノードの複製が作れます。

Duplicator.duplicate(baseGroup);

gridはドラッグ先をグリッド状にする為に使用しますが、シーケンスで指定できるようにしています。例えばこんな感じ。

Draggable {
    grid: [5, 5]
}

ここは柔軟にこう書いてもいいとしました。

Draggable {
    grid: [5] // 5 でもOK
}

X軸とY軸のグリッド幅を変える場合もありますが、正方グリッドの方が使われると思いますので簡単に記述できるようにして、中でごにょごにょしてしまいます。

ドラッグ中のノードはbindを使って位置を調整します。

                    layoutX: bind baseGroup.layoutX, layoutY: bind baseGroup.layoutY
                    translateX: bind dragX, translateY: bind dragY

少し触った間隔ですとlayoutXよりもtranslateXの方がスムーズに移動しているようです。
以下のコードでも動きますが、上記コードの方が意味的にも適切かと思います。

                    layoutX: bind baseGroup.layoutX + dragX, layoutY: bind baseGroup.layoutY + dragY

最後にドラッグ後になにか処理をしたいケースが考えられますので、actionという関数型変数を定義できるようにしました。この関数は新しいノードの座標を引数として受け取り、ドラッグが成立した場合にtrueを返します。したがって、falseを返すことでノードは元の位置にもどります。
このアニメーションはrevert()関数内でTimelineを使って実装しています。

以上、色々とオプションはつけてみましたが、他にも欲しいオプションはあるので暇をみて実装したいかと思います。

*1:MacSafariだとキャッシュがしつこくて中々反映されないですorz