ページ

2013年4月14日日曜日

JavaFX - Dragging gestures (4)

前回は、Full press-drag-release gesture を利用するにあたって、MouseDragEventについてみていきました。今回は、Full press-drag-release gestureを利用して、以下のようなものを作成してみたいと思います。

Paneが2つあり、1つのPaneに配置されているノードをドラッグして、もう1つのPaneに移動するというようなものです。

ノードをドラッグし、マウスをリリースしたときに、もう1つのPane上にある場合は、そのPaneにノードを移動し、それ以外の場合は、もとのPaneに戻っていきます。また、ノードをドラッグ中に、移動先のPane上にきたときは、そのPaneの外枠を赤線で強調表示します。

まずは、以下のようにViewを定義します。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="640" height="480">
    <stylesheets>
        <URL value="@FullPressDragReleaseGestureDemoStyle.css"/>
    </stylesheets>

    <StackPane>
        <GridPane>
            <StackPane GridPane.columnIndex="0"
                       GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
                <StackPane fx:id="leftSidePane" styleClass="pane"
                           prefWidth="200" prefHeight="200"
                           maxWidth="-Infinity" maxHeight="-Infinity">
                    <Circle fx:id="draggableCircle"
                            radius="30"/>
                </StackPane>
            </StackPane>
            
            <StackPane GridPane.columnIndex="1"
                       GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
                <StackPane fx:id="rightSidePane" styleClass="pane"
                           prefWidth="200" prefHeight="200"
                           maxWidth="-Infinity" maxHeight="-Infinity"/>
            </StackPane>
        </GridPane>
        
        <Pane fx:id="dragPane" mouseTransparent="true"/>
    </StackPane>
</Scene>

ノードをドラッグするときに、シーンの最前面で移動したいので、ルートをStackPaneで定義し、ノードをドラッグするためのPaneを最前面になるように設定しています。

次に、ドラッグするノードとそのノードを配置するPaneにイベントハンドラを設定します。

ドラッグするノードについては、以下のようにイベントハンドラを設定します。

private Pane sourcePane;
private double sourceLayoutX;
private double sourceLayoutY;

private DragContext context;

draggableCircle.setOnMousePressed(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        Circle target = (Circle)event.getSource();
        sourcePane = (Pane)target.getParent();
        sourcePane.getChildren().remove(target);
        dragPane.getChildren().add(target);

        Point2D p = dragPane.sceneToLocal(event.getSceneX(), event.getSceneY());
        target.setLayoutX(p.getX() - (event.getX() - target.getCenterX()));
        target.setLayoutY(p.getY() - (event.getY() - target.getCenterY()));
        sourceLayoutX = target.getLayoutX();
        sourceLayoutY = target.getLayoutY();

        context = DragContext.of(target)
            .atMouseSceneLocation(event.getSceneX(), event.getSceneY());
    }
});

draggableCircle.setOnDragDetected(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        ((Node)event.getSource()).startFullDrag();
    }
});

draggableCircle.setOnMouseDragged(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        if (context == null) { return; }
        context.dragNodeForMouseSceneLocation(event.getSceneX(), event.getSceneY());
    }
});

draggableCircle.setOnMouseReleased(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        context = null;
        if (sourcePane == null) { return; }
        animate((Node)event.getSource(), sourcePane, sourcePane);
    }
});

MOUSE_PRESSEDイベントで、対象のノードを現在配置されているPaneから、ドラッグするためのPaneに移動し、そのPane上で、現在配置されいる位置と同じ位置になるように、ノードの位置を設定しています。

DRAG_DETECTEDイベントで、startFullDragメソッドを呼び出して、Full press-drag-release gestureを開始します。

ノードのドラッグに利用しているDragContextについては、こちらを参照してください。

MOUSE_RELEASEDイベントで、ドラッグ終了時の処理を行っています。ドラッグ終了時に、もう1つのPane上にない場合は、元のPaneにノードを戻します。ドラッグ終了時には、MOUSE_DRAG_RELEASED -> MOUSE_RELEASED -> MOUSE_DRAG_EXITEDの順にイベントが発生しますので、もう1つのPane上にノードを移動する場合は、MOUSE_DRAG_RELEASEDイベントで、MOUSE_PRESSEDで設定したsourcePaneをnullにするようにします。そうすると、MOUSE_RELEASEDイベントで、sourcePaneがnullの場合は、もう1つのPane上に移動したことになりますので、何も処理を行いません。逆に、sourcePaneがnullでない場合は、元のPaneに移動する必要があります。元のPaneへの移動は、animateメソッドでアニメーションで移動するようにしています。animateメソッドは、以下のようになります。

private void animate(final Node node, final Pane sourcePane, final Pane destinationPane) {
    double toLayoutX = sourceLayoutX + getPaneHGap(sourcePane, destinationPane);
    double toLayoutY = sourceLayoutY + getPaneVGap(sourcePane, destinationPane);

    ParallelTransitionBuilder.create()
        .children(
            TimelineBuilder.create()
                .keyFrames(
                    new KeyFrame(
                        Duration.millis(400),
                        new KeyValue(node.layoutXProperty(), toLayoutX)
                    )
                )
                .build(),
            TimelineBuilder.create()
                .keyFrames(
                    new KeyFrame(
                        Duration.millis(400),
                        new KeyValue(node.layoutYProperty(), toLayoutY)
                    )
                )
                .build()
        )
        .onFinished(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                dragPane.getChildren().remove(node);
                destinationPane.getChildren().add(node);
            }
        })
        .build()
        .play();
}

private double getPaneHGap(Pane sourcePane, Pane destinationPane) {
    return destinationPane.localToScene(destinationPane.getBoundsInParent()).getMinX() -
        sourcePane.localToScene(sourcePane.getBoundsInParent()).getMinX();
}

private double getPaneVGap(Pane sourcePane, Pane destinationPane) {
    return destinationPane.localToScene(destinationPane.getBoundsInParent()).getMinY() -
        sourcePane.localToScene(sourcePane.getBoundsInParent()).getMinY();
}

アニメーションでノードを移動させ、アニメーションの終了時に、ドラッグのためのPaneから、移動先のPaneにノードを移動させています。

移動先のノードの位置については、2つのPaneの大きさを同じにしていますので、移動元の位置に対して、2つのPaneの位置のギャップを追加するようにしています。

最後に、ノードを配置するPaneのイベントハンドラについて、以下のように設定します。

EventHandler<MouseDragEvent> mouseDragReleaseHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane.equals(targetPane)) { return; }
        animate((Node)event.getGestureSource(), sourcePane, targetPane);
        sourcePane = null;
    }
};

EventHandler<MouseDragEvent> mouseDragEnteredHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane.equals(targetPane)) { return; }
        targetPane.getStyleClass().add("target-pane");
    }
};

EventHandler<MouseDragEvent> mouseDragExitedHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane != null && sourcePane.equals(targetPane)) { return; }
        targetPane.getStyleClass().remove("target-pane");
    }
};

leftSidePane.setOnMouseDragReleased(mouseDragReleaseHandler);
leftSidePane.setOnMouseDragEntered(mouseDragEnteredHandler);
leftSidePane.setOnMouseDragExited(mouseDragExitedHandler);

rightSidePane.setOnMouseDragReleased(mouseDragReleaseHandler);
rightSidePane.setOnMouseDragEntered(mouseDragEnteredHandler);
rightSidePane.setOnMouseDragExited(mouseDragExitedHandler);

MOUSE_DRAG_RELEASEDイベントで、移動元のPaneでない場合は、ノードをイベント発生もとのPaneに移動し、ノードがもう1つのPaneに移動したことを表すために、sourcePaneをnullに設定しています。ノードの移動には、先ほど定義したanimateメソッドを利用しています。

もう1つのPane上に、ドラッグ中のノードが入ってきたときに、外枠を赤色で強調表示するために、MOUSE_DRAG_ENTEREDイベントとMOUSE_DRAG_EXITEDイベントで処理を行っています。外枠を強調表示するには、スタイルクラスを変更することで行っています。スタイルは以下のように定義しています。

.root {
    -fx-base: black;
    -fx-background-color: linear-gradient(to bottom, derive(-fx-base, 60%), derive(-fx-base, 40%));
    -fx-target-border-color: transparent;
}

.pane {
    -fx-background-color:
        -fx-pane-color,
        linear-gradient(to bottom, derive(-fx-pane-color, 60%), derive(-fx-pane-color, 40%));
    -fx-background-insets: 0, 1;
    -fx-background-radius: 10, 9;
    -fx-border-color: -fx-target-border-color;
    -fx-border-width: 5;
    -fx-border-radius: 10;
}

.target-pane {
    -fx-target-border-color: red;
}

#leftSidePane {
    -fx-pane-color: purple;
}

#rightSidePane {
    -fx-pane-color: green;
}

ベースとなるスタイルに、外枠の定義をしておき、MOUSE_DRAG_ENTEREDイベントが発生したときに設定するスタイルで、外枠の色を設定しています。

以上で完了になります。

ノードをドラッグしたときに、他のノードに対して、マウスイベントを発生させたい場合は、DRAG_DETECTEDイベントで、startFullDragメソッドを呼び出して、Full press-drag-release gestureを開始し、MouseDragEventの対象となるイベントに対して、ハンドラを追加すればよいことになります。その場合は、MOUSE_PRESSEDイベントでmouseTransparentプロパティをtrueに設定するようにします。次回は、Platform-supported drag-and-drop gestureについてみていきたいと思います。