JavaFx 8: перемещение компонента между родителями, оставаясь на месте

Мне нужно перемещать узлы (в большинстве случаев обрезанные ImageView) между контейнерами. Во время «родительского изменения» я должен визуально удерживать компоненты на месте, поэтому с точки зрения пользователей эта реорганизация должна быть прозрачной.

Схема моей сцены такая:

введите описание изображения здесь

Узлы, которые мне нужно переместить, обрезаются, транслируются некоторые применяемые к ним эффекты и изначально находятся под панелью Board. Группа contents в desk масштабируется, обрезается и переводится.

Я хочу переместить их в MoverLayer. Работает нормально, потому что moverLayer привязан к Board:

    moverLayer.translateXProperty().bind(board.translateXProperty());
    moverLayer.translateYProperty().bind(board.translateYProperty());
    moverLayer.scaleXProperty().bind(board.scaleXProperty());
    moverLayer.scaleYProperty().bind(board.scaleYProperty());
    moverLayer.layoutXProperty().bind(board.layoutXProperty());
    moverLayer.layoutYProperty().bind(board.layoutYProperty());

поэтому я могу просто перемещать узлы между ними:

public void start(MouseEvent me) {
    board.getContainer().getChildren().remove(node);
    desk.getMoverLayer().getChildren().add(node);
}


public void finish(MouseEvent me) {
    desk.getMoverLayer().getChildren().remove(node);
    board.getContainer().getChildren().add(node);
}

Однако при перемещении узлов между contents tray и MoverLayer это начинает усложняться. Я пробовал играть с разными координатами (локальные, родительские, сцена, экран), но почему-то всегда неуместно. Кажется, что когда масштаб равен 1.0 для desk.contents, он работает, чтобы сопоставить координаты translateX и translateY с координатами экрана, переключить родительский элемент, а затем преобразовать координаты экрана в локальные и использовать в качестве перевода. Но при неидентичном масштабировании координаты различаются (и узел перемещается). Я также попытался рекурсивно сопоставить координаты с общим родителем (desk), но ничего не вышло.

Мой общий вопрос: как лучше всего рассчитывать координаты одной и той же точки относительно разных родителей?

Вот код MCVE. Извините, я просто не мог сделать это проще.

package hu.vissy.puzzlefx.stackoverflow;

import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Mcve extends Application {

    // The piece to move
    private Rectangle piece;

    // The components representing the above structure
    private Pane board;
    private Group moverLayer;
    private Pane trayContents;
    private Pane desk;
    private Group deskContents;

    // The zoom scale
    private double scale = 1;

    // Drag info
    private double startDragX;
    private double startDragY;
    private Point2D dragAnchor;

    public Mcve() {
    }

    public void init(Stage primaryStage) {

        // Added only for simulation
        Button b = new Button("Zoom");
        b.setOnAction((ah) -> setScale(0.8));

        desk = new Pane();
        desk.setPrefSize(800, 600);
        desk.setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY)));

        deskContents = new Group();
        desk.getChildren().add(deskContents);

        board = new Pane();
        board.setPrefSize(700, 600);
        board.setBackground(new Background(new BackgroundFill(Color.LIGHTCORAL, CornerRadii.EMPTY, Insets.EMPTY)));

        // Symbolize the piece to be dragged
        piece = new Rectangle();
        piece.setTranslateX(500);
        piece.setTranslateY(50);
        piece.setWidth(50);
        piece.setHeight(50);
        piece.setFill(Color.BLACK);
        board.getChildren().add(piece);

        // Mover layer is always on top and is bound to the board (used to display the 
        // dragged items above all desk contents during dragging)
        moverLayer = new Group();
        moverLayer.translateXProperty().bind(board.translateXProperty());
        moverLayer.translateYProperty().bind(board.translateYProperty());
        moverLayer.scaleXProperty().bind(board.scaleXProperty());
        moverLayer.scaleYProperty().bind(board.scaleYProperty());
        moverLayer.layoutXProperty().bind(board.layoutXProperty());
        moverLayer.layoutYProperty().bind(board.layoutYProperty());

        board.setTranslateX(50);
        board.setTranslateY(50);

        Pane tray = new Pane();
        tray.setPrefSize(400, 400);
        tray.relocate(80, 80);

        Pane header = new Pane();
        header.setPrefHeight(30);
        header.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, CornerRadii.EMPTY, Insets.EMPTY)));

        trayContents = new Pane();
        trayContents.setBackground(new Background(new BackgroundFill(Color.BEIGE, CornerRadii.EMPTY, Insets.EMPTY)));
        VBox layout = new VBox();
        layout.getChildren().addAll(header, trayContents);
        VBox.setVgrow(trayContents, Priority.ALWAYS);
        layout.setPrefSize(400, 400);

        tray.getChildren().add(layout);
        deskContents.getChildren().addAll(board, tray, moverLayer, b);

        Scene scene = new Scene(desk);

        // Piece is draggable
        piece.setOnMousePressed((me) -> startDrag(me));
        piece.setOnMouseDragged((me) -> doDrag(me));
        piece.setOnMouseReleased((me) -> endDrag(me));

        primaryStage.setScene(scene);
    }

    // Changing the scale
    private void setScale(double scale) {
        this.scale = scale;
        // Reseting piece position and parent if needed
        if (piece.getParent() != board) {
            piece.setTranslateX(500);
            piece.setTranslateY(50);
            trayContents.getChildren().remove(piece);
            board.getChildren().add(piece);
        }
        deskContents.setScaleX(getScale());
        deskContents.setScaleY(getScale());
    }

    private double getScale() {
        return scale;
    }

    private void startDrag(MouseEvent me) {
        // Saving drag options
        startDragX = piece.getTranslateX();
        startDragY = piece.getTranslateY();
        dragAnchor = new Point2D(me.getSceneX(), me.getSceneY());

        // Putting the item into the mover layer -- works fine with all zoom scale level
        board.getChildren().remove(piece);
        moverLayer.getChildren().add(piece);
        me.consume();
    }

    // Doing the drag
    private void doDrag(MouseEvent me) {
        double newTranslateX = startDragX + (me.getSceneX() - dragAnchor.getX()) / getScale();
        double newTranslateY = startDragY + (me.getSceneY() - dragAnchor.getY()) / getScale();

        piece.setTranslateX(newTranslateX);
        piece.setTranslateY(newTranslateY);
        me.consume();
    }

    private void endDrag(MouseEvent me) {
        // For MCVE's sake I take that the drop is over the tray.

        Bounds op = piece.localToScreen(piece.getBoundsInLocal());

        moverLayer.getChildren().remove(piece);
        // One of my several tries: mapping the coordinates till the common parent.
        // I also tried to use localtoScreen -> change parent -> screenToLocal
        Bounds b = localToParentRecursive(trayContents, desk, trayContents.getBoundsInLocal());
        Bounds b2 = localToParentRecursive(board, desk, board.getBoundsInLocal());
        trayContents.getChildren().add(piece);
        piece.setTranslateX(piece.getTranslateX() + b2.getMinX() - b.getMinX() * getScale());
        piece.setTranslateY(piece.getTranslateY() + b2.getMinY() - b.getMinY() * getScale());
        me.consume();
    }


    public static Point2D localToParentRecursive(Node n, Parent parent, double x, double y) {
        // For simplicity I suppose that the n node is on the path of the parent
        Point2D p = new Point2D(x, y);
        Node cn = n;
        while (true) {
            if (cn == parent) {
                break;
            }
            p = cn.localToParent(p);
            cn = cn.getParent();
        }
        return p;
    }

    public static Bounds localToParentRecursive(Node n, Parent parent, Bounds bounds) {
        Point2D p = localToParentRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
        return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        init(primaryStage);

        primaryStage.show();
    }

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

}

person Balage1551    schedule 10.06.2014    source источник
comment
Может помочь mcve.   -  person jewelsea    schedule 11.06.2014
comment
Я добавил код. Но мой общий вопрос остается: я хотел бы переместить узел A с родительского X на родительский Y без изменения внешнего вида (положения, размера). Я могу предположить, что X и Y находятся на одном дереве (имеют общего предка) и даже что они перекрываются в том месте, где находится A.   -  person Balage1551    schedule 11.06.2014


Ответы (1)


Хорошо. После долгой отладки, расчетов и попыток я смог найти решение.

Вот служебная функция, которую я написал для родительского свопа:

/**
 * Change the parent of a node.
 *
 * <p>
 * The node should have a common ancestor with the new parent.
 * </p>
 *
 * @param item
 *            The node to move.
 * @param newParent
 *            The new parent.
 */
@SuppressWarnings("unchecked")
public static void changeParent(Node item, Parent newParent) {
    try {
        // HAve to use reflection, because the getChildren method is protected in common ancestor of all
        // parent nodes.

        // Checking old parent for public getChildren() method
        Parent oldParent = item.getParent();
        if ((oldParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
            throw new IllegalArgumentException("Old parent has no public getChildren method.");
        }
        // Checking new parent for public getChildren() method
        if ((newParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
            throw new IllegalArgumentException("New parent has no public getChildren method.");
        }

        // Finding common ancestor for the two parents
        Parent commonAncestor = findCommonAncestor(oldParent, newParent);
        if (commonAncestor == null) {
            throw new IllegalArgumentException("Item has no common ancestor with the new parent.");
        }

        // Bounds of the item
        Bounds itemBoundsInParent = item.getBoundsInParent();

        // Mapping coordinates to common ancestor
        Bounds boundsInParentBeforeMove = localToParentRecursive(oldParent, commonAncestor, itemBoundsInParent);

        // Swapping parent
        ((Collection<Node>) oldParent.getClass().getMethod("getChildren").invoke(oldParent)).remove(item);
        ((Collection<Node>) newParent.getClass().getMethod("getChildren").invoke(newParent)).add(item);

        // Mapping coordinates back from common ancestor
        Bounds boundsInParentAfterMove = parentToLocalRecursive(newParent, commonAncestor, boundsInParentBeforeMove);

        // Setting new translation
        item.setTranslateX(
                        item.getTranslateX() + (boundsInParentAfterMove.getMinX() - itemBoundsInParent.getMinX()));
        item.setTranslateY(
                        item.getTranslateY() + (boundsInParentAfterMove.getMinY() - itemBoundsInParent.getMinY()));
    } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        throw new IllegalStateException("Error while switching parent.", e);
    }
}

/**
 * Finds the topmost common ancestor of two nodes.
 *
 * @param firstNode
 *            The first node to check.
 * @param secondNode
 *            The second node to check.
 * @return The common ancestor or null if the two node is on different
 *         parental tree.
 */
public static Parent findCommonAncestor(Node firstNode, Node secondNode) {
    // Builds up the set of all ancestor of the first node.
    Set<Node> parentalChain = new HashSet<>();
    Node cn = firstNode;
    while (cn != null) {
        parentalChain.add(cn);
        cn = cn.getParent();
    }

    // Iterates down through the second ancestor for common node.
    cn = secondNode;
    while (cn != null) {
        if (parentalChain.contains(cn)) {
            return (Parent) cn;
        }
        cn = cn.getParent();
    }
    return null;
}

/**
 * Transitively converts the coordinates from the node to an ancestor's
 * coordinate system.
 *
 * @param node
 *            The node the starting coordinates are local to.
 * @param ancestor
 *            The ancestor to map the coordinates to.
 * @param x
 *            The X of the point to be converted.
 * @param y
 *            The Y of the point to be converted.
 * @return The converted coordinates.
 */
public static Point2D localToParentRecursive(Node node, Parent ancestor, double x, double y) {
    Point2D p = new Point2D(x, y);
    Node cn = node;
    while (cn != null) {
        if (cn == ancestor) {
            return p;
        }
        p = cn.localToParent(p);
        cn = cn.getParent();
    }
    throw new IllegalStateException("The node is not a descedent of the parent.");
}

/**
 * Transitively converts the coordinates of a bound from the node to an
 * ancestor's coordinate system.
 *
 * @param node
 *            The node the starting coordinates are local to.
 * @param ancestor
 *            The ancestor to map the coordinates to.
 * @param bounds
 *            The bounds to be converted.
 * @return The converted bounds.
 */
public static Bounds localToParentRecursive(Node node, Parent ancestor, Bounds bounds) {
    Point2D p = localToParentRecursive(node, ancestor, bounds.getMinX(), bounds.getMinY());
    return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}

/**
 * Transitively converts the coordinates from an ancestor's coordinate
 * system to the nodes local.
 *
 * @param node
 *            The node the resulting coordinates should be local to.
 * @param ancestor
 *            The ancestor the starting coordinates are local to.
 * @param x
 *            The X of the point to be converted.
 * @param y
 *            The Y of the point to be converted.
 * @return The converted coordinates.
 */
public static Point2D parentToLocalRecursive(Node n, Parent parent, double x, double y) {
    List<Node> parentalChain = new ArrayList<>();
    Node cn = n;
    while (cn != null) {
        if (cn == parent) {
            break;
        }
        parentalChain.add(cn);
        cn = cn.getParent();
    }
    if (cn == null) {
        throw new IllegalStateException("The node is not a descedent of the parent.");
    }

    Point2D p = new Point2D(x, y);
    for (int i = parentalChain.size() - 1; i >= 0; i--) {
        p = parentalChain.get(i).parentToLocal(p);
    }

    return p;
}

/**
 * Transitively converts the coordinates of the bounds from an ancestor's
 * coordinate system to the nodes local.
 *
 * @param node
 *            The node the resulting coordinates should be local to.
 * @param ancestor
 *            The ancestor the starting coordinates are local to.
 * @param bounds
 *            The bounds to be converted.
 * @return The converted coordinates.
 */
public static Bounds parentToLocalRecursive(Node n, Parent parent, Bounds bounds) {
    Point2D p = parentToLocalRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
    return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}

Вышеупомянутое решение работает хорошо, но мне интересно, является ли это самым простым способом выполнить задачу. Для общности мне пришлось использовать некоторую рефлексию: метод getChildren() класса Parent защищен, только его потомки делают его общедоступным, если хотят, поэтому я не могу вызвать его напрямую через Parent.

Использовать указанную выше утилиту просто: вызовите changeParent( node, newParent ).

Эта утилита также дает функцию для поиска общего предка двух узлов и рекурсивного преобразования координат через предковую цепочку узлов в и из.

person Balage1551    schedule 12.06.2014