JavaFx 8:在父母居住期间移动组件

我需要在容器之间移动节点(mosty剪切的
ImageViews).在“父变更”期间,我必须在视觉上保持组件的位置,因此从用户的角度来看,这种重组应该是透明的.

我的场景架构是这样的:

我必须移动的节点被剪切,翻译了一些应用于它们的效果,并且最初位于Board面板下.桌面的内容组被缩放,剪裁和翻译.

我想将它们移到MoverLayer.它工作正常,因为moverLayer被绑定到董事会:

    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);
}

但是,当在托盘和MoverLayer的内容之间移动节点时,它开始变得复杂.我尝试使用不同的坐标(本地,父母,场景,屏幕),但不知何故,它总是放错地方.看来,当desk.contents的比例为1.0时,它可以将translateX和translateY的坐标映射到屏幕坐标,切换父级,然后将屏幕坐标映射回本地并用作翻译.但是,如果缩放比例不同,则坐标会有所不同(并且节点会移动).我还尝试递归地将坐标映射到公共父(桌面),但两者都不起作用.

我的一般性问题是,计算相对于不同父母的相同点的坐标的最佳实践是什么?

这是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);
    }

}

最佳答案 好.经过大量的调试,计算并尝试我能够找到解决方案.

这是我为执行父交换而编写的实用程序函数:

/**
 * 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());
}

上面的解决方案运行良好,但我想知道它是否是执行任务的最简单方法.为了通用性,我不得不使用一些反射:Parent类的getChildren()方法受到保护,只有它的后代才会公开,所以我不能直接通过Parent调用它.

使用上面的实用程序很简单:调用changeParent(node,newParent).

这个实用程序还提供了查找两个节点的共同祖先的功能,并通过节点祖先链递归地转换坐标.

点赞