JavaFX MouseEvent 定位精度随时间降低,导致节点移动抖动

JavaFX MouseEvent location accuracy degrades over time, results in node movement jittering

我不明白为什么会这样。目前我正在尝试通过右键单击一个节点 (Canvas) 来拖动它,按住右键并移动鼠标。一开始它很流畅,但随后开始出现这种奇怪的抖动。这仅在我按住鼠标按钮时发生。如果你释放它,它就会恢复正常(但很快就会再次紧张)。

通过一些调试,您移动它的次数越多,您就会在每次拖动事件之间获得此 'imprecision',鼠标位置越来越不同步。最后的结果就是来回拖,看起来很糟糕

正在使用的代码:

Main.java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("CanvasPane.fxml"));
            Pane root = fxmlLoader.load();
            Scene scene = new Scene(root, 512, 512);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

Controller.java

import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;

public class Controller {

    @FXML
    private Pane rootPane;

    @FXML
    private Canvas canvas;

    private double mouseX;

    private double mouseY;

    @FXML
    private void initialize() {
        GraphicsContext gc = canvas.getGraphicsContext2D();

        // Set the writing pen.
        gc.setLineCap(StrokeLineCap.ROUND);
        gc.setLineJoin(StrokeLineJoin.ROUND);
        gc.setLineWidth(1);
        gc.setStroke(Color.BLACK);

        // Set the background to be transparent.
        gc.setFill(Color.BLUE);
        gc.fillRect(0, 0, 256, 256);

        // Handle moving the canvas.
        canvas.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.moveTo(e.getX(), e.getY());
            } else if (e.getButton() == MouseButton.SECONDARY) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });

        canvas.setOnMouseDragged(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(e.getX(), e.getY());
                gc.stroke();
            } else if (e.getButton() == MouseButton.SECONDARY) {
                double newMouseX = e.getX();
                double newMouseY = e.getY();
                double deltaX = newMouseX - mouseX;
                double deltaY = newMouseY - mouseY;
                canvas.setLayoutX(canvas.getLayoutX() + deltaX);
                canvas.setLayoutY(canvas.getLayoutY() + deltaY);
                mouseX = newMouseX;
                mouseY = newMouseY;
                // Debug: Why is this happening?
                if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
                    System.out.println(deltaX + " " + deltaY);
            }
        });
    }
}

CanvasPane.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.Pane?>

<Pane fx:id="rootPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="0.0" minWidth="0.0" prefHeight="512.0" prefWidth="512.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="your-controller-here">
   <children>
      <Canvas fx:id="canvas" height="256.0" layoutX="122.0" layoutY="107.0" width="256.0" />
   </children>
</Pane>

使用说明

1) 运行 申请

2) 右键单击​​ canvas 并稍微移动它(比如每秒只移动一个像素)

3) 移动得快一点,以便调试打印

4) 当你回到缓慢移动时,由于某种原因你会看到它来回跳跃(比如新旧位置之间的增量移动大于 5 像素)即使你只是移动它一个像素。

然后它似乎会在您下次移动时尝试自行修复,这使它看起来很丑陋。


我感到困惑的原因是,有时它工作得很好,然后当你继续拖动时准确度就会下降。

我是不是编码有误,或者这是一个潜在的错误?

您使用 MouseEvent.getX()MouseEvent.getY() 测量的坐标是相对于事件发生的节点:即相对于您的 Canvas。由于您随后移动了 canvas,旧坐标现在不正确(因为它们是相对于旧位置,而不是新位置)。因此,deltaXdeltaY 的值不正确。

要解决此问题,只需相对于 "fixed" 的某物进行测量,例如Scene。您可以使用 MouseEvent.getSceneX()MouseEvent.getSceneY().

来完成此操作
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;

public class CanvasDragController {

    @FXML
    private Pane rootPane;

    @FXML
    private Canvas canvas;

    private double mouseX;

    private double mouseY;

    @FXML
    private void initialize() {
        GraphicsContext gc = canvas.getGraphicsContext2D();

        // Set the writing pen.
        gc.setLineCap(StrokeLineCap.ROUND);
        gc.setLineJoin(StrokeLineJoin.ROUND);
        gc.setLineWidth(1);
        gc.setStroke(Color.BLACK);

        // Set the background to be transparent.
        gc.setFill(Color.BLUE);
        gc.fillRect(0, 0, 256, 256);

        // Handle moving the canvas.
        canvas.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.moveTo(e.getX(), e.getY());
            } else if (e.getButton() == MouseButton.SECONDARY) {
                mouseX = e.getSceneX();
                mouseY = e.getSceneY();
            }
        });

        canvas.setOnMouseDragged(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(e.getX(), e.getY());
                gc.stroke();
            } else if (e.getButton() == MouseButton.SECONDARY) {
                double newMouseX = e.getSceneX();
                double newMouseY = e.getSceneY();
                double deltaX = newMouseX - mouseX;
                double deltaY = newMouseY - mouseY;
                canvas.setLayoutX(canvas.getLayoutX() + deltaX);
                canvas.setLayoutY(canvas.getLayoutY() + deltaY);
                mouseX = newMouseX;
                mouseY = newMouseY;
                // Why is this happening?
                if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
                    System.out.println(deltaX + " " + deltaY);
            }
        });
    }
}

另一种方法(至少对我来说不太直观)是使用 MouseEvent.getX()MouseEvent.getY() 但不更新鼠标的 "last coordinates"。考虑这一点的方式是,当节点被拖动时,您希望它不相对于鼠标移动 - 换句话说,您希望节点移动以便鼠标的坐标相对于它保持固定.这个版本看起来像:

import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;

public class CanvasDragController {

    @FXML
    private Pane rootPane;

    @FXML
    private Canvas canvas;

    private double mouseX;

    private double mouseY;

    @FXML
    private void initialize() {
        GraphicsContext gc = canvas.getGraphicsContext2D();

        // Set the writing pen.
        gc.setLineCap(StrokeLineCap.ROUND);
        gc.setLineJoin(StrokeLineJoin.ROUND);
        gc.setLineWidth(1);
        gc.setStroke(Color.BLACK);

        // Set the background to be transparent.
        gc.setFill(Color.BLUE);
        gc.fillRect(0, 0, 256, 256);

        // Handle moving the canvas.
        canvas.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.moveTo(e.getX(), e.getY());
            } else if (e.getButton() == MouseButton.SECONDARY) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });

        canvas.setOnMouseDragged(e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(e.getX(), e.getY());
                gc.stroke();
            } else if (e.getButton() == MouseButton.SECONDARY) {
                double newMouseX = e.getX();
                double newMouseY = e.getY();
                double deltaX = newMouseX - mouseX;
                double deltaY = newMouseY - mouseY;
                canvas.setLayoutX(canvas.getLayoutX() + deltaX);
                canvas.setLayoutY(canvas.getLayoutY() + deltaY);
                // Why is this happening?
                if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
                    System.out.println(deltaX + " " + deltaY);
            }
        });
    }
}