Drag and Drop on JavaFX Part 4 (Puzzle)
今回も懲りずに Drag and Drop です。
今回は 前回 紹介した Sortable を改良して、
script.aculo.us の Puzzle Demo を
JavaFX で実装してみました。
前回は VBox でしたが、今回はパズルをレイアウトするのに、Tile を使うため、
- Y 方向だけでなく、X 方向にも Drag できるようした。
- X 方向の overlap を比較するようにした。(前回は Y 方向のみ)
- Drop 後 X 方向の移動量も 0 に戻すようにした。
- 子 Node の id 属性の値のシーケンス(表示の順に並べた)を返す sequence() を追加した。(script.aculo.us と同じ)
サンプル
バラバラになった9分割の画像を Drag and Drop で移動して元に戻すという簡単な絵合わせパズルです。
script.aculo.us の Puzzle 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 } } }