ページ

2013年4月7日日曜日

JavaFX - Dragging gestures (2)

前回は、Simple press-drag-release gestureを使って、ドラッグでノードを移動する動作についてみていきました。今回は、この動作を行うユーティリティクラスを作成してみたいと思います。また、前回は、マウスのカーソルがシーンの外に移動した場合に、ノードがシーンの外に出てしまっていましたので、今回はその部分についても改善してみたいと思います。

まずは、ドラッグに関する情報を保持するDragContextクラスを作成します。このクラスでは、ドラッグ対象のノードとMOUSE_PRESSEDイベント発生時の位置を保持し、MOUSE_DRAGGEDイベント時のマウスのシーン上での位置を指定して、ノードの位置を変更するようにします。また、マウスのカーソルが移動できる領域を表すノードを指定して、その領域内でのみドラッグできるようにもします。

DragContextクラスのインスタンスの生成は、MOUSE_PRESSEDイベント発生時に、以下のようにして生成できるようにします。

DragContext context = DragContext.of(node)
    .atMouseSceneLocation(sceneX, sceneY);

また、マウスのカーソルが移動できる領域を表すノードを指定する場合は、以下のようにして生成できるようにします。

DragContext context = DragContext.of(node)
    .boundTo(boundingNode)
    .atMouseSceneLocation(sceneX, sceneY);

MOUSE_DRAGGEDイベント発生時には、以下のようにして、ノードを移動します。

context.dragNodeForMouseSceneLocation(sceneX, sceneY);

このとき、マウスのカーソルが移動できる領域を表すノードを指定している場合は、マウスのカーソルがそのノードの外にある場合は、ノードの位置を移動しないようにします。また、マウスのカーソルが移動できる領域を表すノードを指定しなかった場合は、マウスのカーソルがシーンの外にある場合は、ノードの位置を移動しないようにします。

public final class DragContext {
    private final Node dragNode;
    private final Node boundingNode;
    private final double dragAnchorX;
    private final double dragAnchorY;

    private DragContext(Builder builder) {
        this.dragNode = builder.dragNode;
        this.boundingNode = builder.boundingNode;
        this.dragAnchorX = builder.dragAnchorX;
        this.dragAnchorY = builder.dragAnchorY;
    }

    public static Builder of(Node node) {
        return new Builder(Objects.requireNonNull(node));
    }

    public void dragNodeForMouseSceneLocation(double sceneX, double sceneY) {
        if (!canDragNodeForMouseSceneLocation(sceneX, sceneY)) { return; }

        dragNode.setLayoutX(sceneX - dragAnchorX);
        dragNode.setLayoutY(sceneY - dragAnchorY);
    }

    private boolean canDragNodeForMouseSceneLocation(double sceneX, double sceneY) {
        if (boundingNode != null) {
            return boundingNode.getLayoutBounds().contains(
                boundingNode.sceneToLocal(sceneX, sceneY)
            );
        }

        Scene scene = dragNode.getScene();
        Bounds sceneBounds = new BoundingBox(0, 0, scene.getWidth(), scene.getHeight());
        return sceneBounds.contains(sceneX, sceneY);
    }

    public static final class Builder {
        private final Node dragNode;
        private Node boundingNode;
        private double dragAnchorX;
        private double dragAnchorY;

        private Builder(Node node) {
            dragNode = node;
        }

        public Builder boundTo(Node node) {
            boundingNode = node;
            return this;
        }

        public DragContext atMouseSceneLocation(double sceneX, double sceneY) {
            dragAnchorX = sceneX - dragNode.getLayoutX();
            dragAnchorY = sceneY - dragNode.getLayoutY();
            return new DragContext(this);
        }
    }
}

次に、ノードのマウスイベントにハンドラを追加し、ドラッグ時にノードの移動を行う処理を行うDraggableクラスを作成します。

Draggableクラスのインスタンスの生成は、ドラッグ対象のノードを指定して、以下のように生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .enable();

また、マウスのカーソルが移動できる領域を表すノードを指定する場合は、以下のようにして生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .boundTo(bindingNode)
    .enable();

マウスのカーソルが移動できる領域を表すノードが、ドラッグ対象のノードの親の場合は、以下のようにして生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .boundToParent()
    .enable();

また、FXMLで、以下のように指定できるように、DragNodeプロパティとBoundingNodeプロパティを定義しておきます。

<Draggable dragNode="$dragNode" boundingNode="$boundingNode"/>

さらに、マウスのカーソルが移動できる領域を表すノードを指定しない(マウスのカーソルが移動できる領域をシーン内とする)場合は、以下のようにスタティックプロパティで指定できるようにもしておきます。

<Node Draggable.enable="true"/>

あとは、DragNodeプロパティにノードを指定したときに、MOUSE_PRESSED, MOUSE_DRAGGED, MOUSE_RELEASEDイベントに、DragContextを使ってノードを移動するハンドラを追加します。

上記のようにして利用できるDraggableクラスを使って、以下のような、ノードの位置をドラッグで移動するものを作成してみます。

左の赤のCircleでは、マウスのカーソルが移動できる領域をCircleの親のノードに指定し、右の青のCircleでは、マウスのカーソルが移動できる領域を指定していません。

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

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

    <HBox>
        <Pane fx:id="boundingPane" styleClass="pane">
            <Circle fx:id="boundedDraggableCircle"
                    centerX="300" centerY="200" radius="30"/>
            <fx:define>
                <Draggable dragNode="$boundedDraggableCircle" boundingNode="$boundingPane"/>
            </fx:define>
        </Pane>

        <Pane HBox.hgrow="ALWAYS" styleClass="pane">
            <Circle fx:id="unboundedDraggableCircle"
                    Draggable.enable="true"
                    centerX="100" centerY="50" radius="30"/>
        </Pane>
    </HBox>
</Scene>

最初のCircleでは、DraggableクラスのインスタンスにDragNodeとBoundingNodeを指定し、次のCircleでは、スタティックプロパティで指定しています。

次に、コントローラクラスを定義します。

public class DraggableDemoSceneController {
    @FXML
    private Scene scene;

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Draggable Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }
}

Draggableクラスで、ドラッグ時の処理を行っていますので、コントローラクラスでは特に何も処理を行いません。

あとは、Applicationクラスを作成しておきます。

public class DraggableDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        FXController.of(new DraggableDemoSceneController())
            .fromDefaultLocation()
            .load()
            .performOn(primaryStage);
    }

    public static void main(String... args) {
        launch(args);
    }
}

最後に、以下のように、複数のノードを作成して、それらをドラッグで移動するものを作成してみます。

右クリックで、Circleをクリックした位置を中心に作成し、それをドラッグで移動できるようにしています。

public class SimplePressDragReleaseGestureDemo extends Application {
    private final Random random = new Random();

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(makeScene());
        stage.setTitle("Simple press-drag-release gesture Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    private Scene makeScene() {
        return SceneBuilder.create()
            .stylesheets(getClass().getResource("SimplePressDragReleaseGestureDemoStyle.css").toExternalForm())
            .width(640).height(480)
            .root(
                PaneBuilder.create()
                    .onMouseClicked(new EventHandler() {
                        @Override
                        public void handle(MouseEvent mouseEvent) {
                            if (mouseEvent.getButton() != MouseButton.SECONDARY) { return; }

                            Pane pane = (Pane)mouseEvent.getSource();
                            Color color = Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
                            Circle circle = CircleBuilder.create()
                                .centerX(mouseEvent.getX()).centerY(mouseEvent.getY())
                                .radius(30)
                                .stroke(color)
                                .fill(
                                    RadialGradientBuilder.create()
                                        .centerX(0.3).centerY(0.3)
                                        .stops(
                                            StopBuilder.create()
                                                .color(color.brighter())
                                                .offset(0)
                                                .build(),
                                            StopBuilder.create()
                                                .color(color.darker())
                                                .offset(1)
                                                .build()
                                        )
                                        .build()
                                )
                                .scaleX(0).scaleY(0)
                                .build();
                            Draggable.setEnable(circle, true);
                            pane.getChildren().add(circle);
                            ScaleTransitionBuilder.create()
                                .node(circle)
                                .toX(1).toY(1)
                                .duration(Duration.millis(300))
                                .build()
                                .play();
                        }
                    })
                    .build()
            )
            .build();
    }

    public static void main(String... args) {
        launch(args);
    }
}

Simple press-drag-release gestureを使って、ドラッグでノードを移動する動作を行うユーティリティクラスを作成してみました。ノードにこの動作を指定する方法として、Draggableクラスのインスタンスにノードを指定する場合と、スタティックプロパティで指定する場合の両方で指定できるようにしてみました。ノードに対して、何か振る舞いを追加したい場合は、これらの方法を利用することができますので、他の振る舞いを追加したい場合にも使ってみたいと思います。次回は、Full press-drag-release gestureについてみていきたいと思います。