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

我的场景的架构是这样的:

我必须移动的节点被剪裁,转换了一些应用于它们的效果,并且最初位于 Board Pane 下。 contentsdesk 组被缩放、裁剪和翻译。

我想将它们移动到 MoverLayer。它工作正常,因为 moverLayer 绑定(bind)到 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);
}

然而,当在 contentstrayMoverLayer 之间移动节点时,它开始变得复杂。我尝试使用不同的坐标(本地、父级、场景、屏幕),但不知何故它总是放错地方。看起来,当 desk.contents 的比例为 1.0 时,它可以将 translateXtranslateY 的坐标映射到屏幕坐标,切换父级,然后将屏幕坐标映射回本地并用作翻译。但是对于不同的缩放,坐标不同(并且节点移动)。我还尝试以递归方式将坐标映射到公共(public)父级( desk ),但两者都不起作用。

我的一般问题是,计算同一点相对于不同 parent 的坐标的最佳做法是什么?

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

上述解决方案效果很好,但我想知道这是否是完成任务的最简单方法。为了一般性,我不得不使用一些反射:getChildren() 类的 Parent 方法是 protected ,只有它的后代愿意将其公开,所以我不能直接通过 Parent 调用它。

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

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

关于JavaFx 8 : Moving component between parents while staying in place,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/24150931/

10-11 20:18