Drag and Drop on JavaFX Part 4 (Puzzle)

今回も懲りずに Drag and Drop です。
今回は 前回 紹介した Sortable を改良して、 script.aculo.usPuzzle DemoJavaFX で実装してみました。

前回は VBox でしたが、今回はパズルをレイアウトするのに、Tile を使うため、

  • Y 方向だけでなく、X 方向にも Drag できるようした。
  • X 方向の overlap を比較するようにした。(前回は Y 方向のみ)
  • Drop 後 X 方向の移動量も 0 に戻すようにした。
  • 子 Node の id 属性の値のシーケンス(表示の順に並べた)を返す sequence() を追加した。(script.aculo.us と同じ)
のような簡単な改良を行いました。

サンプル

バラバラになった9分割の画像を Drag and Drop で移動して元に戻すという簡単な絵合わせパズルです。
script.aculo.usPuzzle Demo をほぼそのまま再現してみました。
もちろん、イメージも拝借...

このパズル 実は iPhone のメニューを移動する時と同じ動きをします。
画像をアプリケーションのアイコンに変更すれば、簡単に iPhone ライクなメニューができそうですね。

Main.fx
前回と同様、最後に Sortable を生成するだけです。
var puzzle: Tile;
var info: String;
var moves: Integer;

Stage {
    title: "Puzzle Demo"
    resizable: false
    scene: Scene {
        width: 450
        height: 475
        content: [
            puzzle = Tile {
                columns: 3
                rows: 3
                content: Sequences.shuffle(for (i in [1..9]) {
                    ImageView {
                        id: "{i}"
                        image: Image { url: "{__DIR__}puzzle{i}.jpg" }
                        onMouseReleased: function(e: MouseEvent) {
                            info = if ("{sortable.sequence()}" == "123456789") {
                                "You've solved the puzzle in {++moves} moves!";
                            } else {
                                "You've made {++moves} move{if (moves > 1) 's' else ''}";
                            }
                        }
                    }
                }) as ImageView[]
            }
            Text { content: bind info translateX: 5 translateY: 468}
        ]
    }
}

var sortable: Sortable = Sortable { node: puzzle }
Sortable.fx
public class Sortable {
    public-init var node: Group;
    public-init var handle: String;
    public-init var constraint: String;

    var dummy = Rectangle { fill: Color.WHITE };
    var draggedNode: Node;
    var draggedIndex = 0;

    postinit {
        for (_node in node.content) {
            Draggable {
                handle: handle
                constraint: constraint
                node: _node

                // dummy ノード 準備
                onMousePressed: function(e: MouseEvent) {
                    draggedNode = e.source;
                    draggedIndex = Sequences.indexByIdentity(
                                            node.content, draggedNode);

                    draggedNode.layoutInfo = LayoutInfo { managed: false };
                    draggedNode.toFront();

                    insert dummy before node.content[draggedIndex];
                    dummy.width  = draggedNode.boundsInLocal.width;
                    dummy.height = draggedNode.boundsInLocal.height;
                }

                // dummy ノード 移動
                // ドラッグした Node が 他の Node と 50% 以上 重なった場合に dummy を移動する。
                onMouseDragged: function(e: MouseEvent) {
                    for (_node in node.content) {
                        if (overlap(draggedNode, _node, X) > 0.5
                            and overlap(draggedNode, _node, Y) > 0.5) {
                            var index = indexof _node;
                            if (index != draggedIndex) {
                                var dummy = node.content[draggedIndex];
                                delete node.content[draggedIndex];
                                insert dummy before node.content[index];
                                draggedIndex = index;
                           }
                            break;
                        }
                    }
                }

                // Drop 処理
                onMouseReleased: function(e: MouseEvent) {
                    delete draggedNode from node.content;
                    node.content[draggedIndex] = draggedNode;

                    draggedNode.translateY = 0.0;
                    draggedNode.layoutInfo = LayoutInfo { managed: true };

                    draggedNode = null;
                    draggedIndex = -1;
                }
            }
        }
    }

    public function sequence() {
        return for (n in node.content) n.id;
    }

    function overlap(n1: Node, n2: Node, mode: String): Number {
        var b1 = n1.boundsInParent;
        var min1 = if (mode == X) b1.minX else if (mode == Y) b1.minY else 0.0;
        var max1 = if (mode == X) b1.maxX else if (mode == Y) b1.maxY else 0.0;

        var b2 = n2.boundsInParent;
        var min2 = if (mode == X) b2.minX else if (mode == Y) b2.minY else 0.0;
        var max2 = if (mode == X) b2.maxX else if (mode == Y) b2.maxY else 0.0;

        if (min2 < min1 and min1 < max2) {
            (max2 - min1) / (max1 - min1)
        } else if (min2 < max1 and max1 < max2) {
            (max1 - min2) / (max1 - min1)
        } else {
            0.0
        }
    }
}