如何在移动节点时更改 viewportBounds 后使光标停留在节点的边界内
How to make the cursor stay within the bounds of a node after changing viewportBounds while moving that node
我有可以移动的节点,这些节点放置在 ScrollPane.When 上的窗格上 我将一个节点拖到滚动窗格的 viewportBounds
之外, vvalue
应该改变,以便节点再次在这些范围内。为了解决这个问题,我尝试使用 this question.
的答案
我的问题是当节点再次在viewportBounds
的边界内后,光标相对于节点移动,如果我想继续将节点移动到视口之外,经过几次迭代光标会移动太多以至于它会超出整个应用程序 window 并且会靠在屏幕边界上。如何保持光标在节点上的位置?
如果您想测试代码,请记住 viewport
边界的重构仅在您沿 Y 轴移动节点时发生。
public class NewFXMain extends Application {
@Override
public void start(Stage primaryStage) {
AnchorPane root = new AnchorPane();
ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000,5000);
Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
DraggableNode node = new DraggableNode(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node.setStyle(
"-fx-background-color: #334488; "
+ "-fx-text-fill: black; "
+ "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class DraggableNode extends Pane {
private double x = 0;
private double y = 0;
private double mousex = 0;
private double mousey = 0;
private Node view;
private boolean dragging = false;
private boolean moveToFront = true;
private void ensureVisible(ScrollPane scrollPane) {
Bounds viewport = scrollPane.getViewportBounds();
double contentHeight = scrollPane.getContent().localToScene(scrollPane.getContent().getBoundsInLocal()).getHeight();
double nodeMinY = this.localToScene(this.getBoundsInLocal()).getMinY();
double nodeMaxY = this.localToScene(this.getBoundsInLocal()).getCenterY();
double vValueDelta = 0;
double vValueCurrent = scrollPane.getVvalue();
if (nodeMaxY < 0) {
// currently located above (remember, top left is (0,0))
vValueDelta = (nodeMinY) / contentHeight;
System.out.println("FIRST CASE DELTA: " + vValueDelta);
} else if (nodeMinY > viewport.getHeight()) {
// currently located below
vValueDelta = ((nodeMinY) / contentHeight) / 5;
System.out.println("SECOND CASE DELTA: " + vValueDelta);
}
scrollPane.setVvalue(vValueCurrent + vValueDelta);
}
public DraggableNode(ScrollPane pane) {
init(pane);
}
private void init(ScrollPane scroll) {
onMousePressedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// record the current mouse X and Y position on Node
mousex = event.getSceneX();
mousey = event.getSceneY();
x = getLayoutX();
y = getLayoutY();
if (isMoveToFront()) {
toFront();
}
}
});
//Event Listener for MouseDragged
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// Get the exact moved X and Y
double offsetX = event.getSceneX() - mousex;
double offsetY = event.getSceneY() - mousey;
x += offsetX;
y += offsetY;
double scaledX = x;
double scaledY = y;
setLayoutX(scaledX);
setLayoutY(scaledY);
ensureVisible(scroll);
dragging = true;
// again set current Mouse x AND y position
mousex = event.getSceneX();
mousey = event.getSceneY();
event.consume();
}
});
onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
dragging = false;
}
});
}
public void setMoveToFront(boolean moveToFront) {
this.moveToFront = moveToFront;
}
public boolean isMoveToFront() {
return moveToFront;
}
}
回答你的问题:
How do I maintain position of cursor on the node?
您需要使用 AWT Robot class 来移动光标。
private void movePointer(Node node){
Bounds b = node.localToScreen(node.getLayoutBounds());
try {
final Robot robot = new Robot();
robot.mouseMove((int) (b.getMinX()+(b.getWidth()/2)), (int) (b.getMinY()+(b.getHeight()/2)));
} catch (final AWTException e) {
System.out.println("Unable to poistion the pointer");
}
}
上述方法会将光标定位在提供的节点的中心。设置vValue后即可调用上述方法。
话虽如此,从用户的角度来看,这是非常奇怪的行为
- 让节点超出视口边界然后跳回
突然让它可见
- 跳转光标
所以我尝试以一种方式实现,拖动的节点 永远不会 超出视口边界并且 vValue/hValue 根据拖动调整并维护节点在视口中可见。
为了解决光标到达 app/screen 边界的另一个问题,我实现了一种在拖动时自动开始滚动的方法:一旦光标超出视口 bounds.When,光标就会返回视口边界,它只是作为一个正常的拖动操作。
下面是具有所需更改的代码的快速演示。您可以根据自己的需要进行微调。
我在窗格中添加了背景以展示正在进行的拖动操作;)
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.Pair;
public class NewFXMainV1 extends Application {
public static void main(final String[] args) {
launch(args);
}
@Override
public void start(final Stage primaryStage) {
final AnchorPane root = new AnchorPane();
// Adding a pattern to background to observe the automatic drag :)
String bg1 = "linear-gradient(from 0% 0% to 1% 0% , repeat, #DDDDDD 50% , transparent 22% )";
String bg2 = "linear-gradient(from 0% 0% to 0% 1% , repeat, transparent 50% , #DDDDDD 22% )";
root.setStyle("-fx-background-color:" + bg1 + ", " + bg2 + ";");
final ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000, 5000);
final Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
final DraggableNodeV1 node = new DraggableNodeV1(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node
.setStyle(
"-fx-background-color: #334488; " + "-fx-text-fill: black; " + "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
}
class DraggableNodeV1 extends Pane {
private static Duration d = Duration.millis(26);
private static double PX_JUMP = 10;
private static double AUTOMATIC_OFFSET = 15;
private double x = 0;
private double y = 0;
private double mouseX = 0;
private double mouseY = 0;
private Timeline xTimeline;
private Timeline yTimeline;
public DraggableNodeV1(final ScrollPane pane) {
init(pane);
}
private void ensureVisible(final ScrollPane scrollPane) {
final Pane dragNode = this;
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final Bounds contentBounds = scrollPane.getContent().getLayoutBounds();
final double layoutY = dragNode.getLayoutY();
final double layoutX = dragNode.getLayoutX();
final double visibleYInViewport = vpMinY + (vpHeight - dragNode.getHeight());
double totalHeightToScroll = contentBounds.getHeight() - vpHeight;
if (layoutY > visibleYInViewport) {
double heightToScroll = vpMinY + (layoutY - visibleYInViewport);
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
} else if (layoutY < vpMinY) {
double heightToScroll = layoutY;
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
}
final double visibleXInViewport = vpMinX + (vpWidth - dragNode.getWidth());
double totalWidthToScroll = contentBounds.getWidth() - vpWidth;
if (layoutX > visibleXInViewport) {
double widthToScroll = vpMinX + (layoutX - visibleXInViewport);
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
} else if (layoutX < vpMinX) {
double widthToScroll = layoutX;
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
}
}
private void init(final ScrollPane scroll) {
onMousePressedProperty().set(event -> {
mouseX = event.getScreenX();
mouseY = event.getScreenY();
x = getLayoutX();
y = getLayoutY();
});
// Event Listener for MouseDragged
onMouseDraggedProperty().set(event -> {
if (isMouseInDraggableViewPort(event, scroll)) {
dragTheNode(event, scroll);
clearTimelines();
} else {
Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> auto = isMouseInAutomaticDrag(event, scroll);
Pair<Boolean, Boolean> xAuto = auto.getKey();
if (xAuto.getKey() || xAuto.getValue()) {
if (xAuto.getKey()) { // towards left
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutX() > 0) {
setLayoutX(getLayoutX() - PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
} else if (xAuto.getValue()) { // towards right
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutX() < contentBounds.getWidth() - getWidth()) {
setLayoutX(getLayoutX() + PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
}
} else {
stopXTimeline();
}
Pair<Boolean, Boolean> yAuto = auto.getValue();
if (yAuto.getKey() || yAuto.getValue()) {
if (yAuto.getKey()) { // towards top
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutY() > 0) {
setLayoutY(getLayoutY() - PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
} else if (yAuto.getValue()) { // towards bottom
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutY() < contentBounds.getHeight() - getHeight()) {
setLayoutY(getLayoutY() + PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
}
} else {
stopYTimeline();
}
}
event.consume();
});
onMouseReleasedProperty().set(event -> clearTimelines());
}
private void clearTimelines() {
stopXTimeline();
stopYTimeline();
}
private void stopXTimeline() {
if (xTimeline != null) {
xTimeline.stop();
}
xTimeline = null;
}
private void stopYTimeline() {
if (yTimeline != null) {
yTimeline.stop();
}
yTimeline = null;
}
private Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> isMouseInAutomaticDrag(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
double eX = event.getSceneX();
double eY = event.getSceneY();
double vpMinX = viewportSceneBounds.getMinX();
double vpMaxX = viewportSceneBounds.getMaxX();
double vpMinY = viewportSceneBounds.getMinY();
double vpMaxY = viewportSceneBounds.getMaxY();
Pair<Boolean, Boolean> autoX = new Pair<>(eX <= vpMinX + AUTOMATIC_OFFSET, vpMaxX - AUTOMATIC_OFFSET <= eX);
Pair<Boolean, Boolean> autoY = new Pair<>(eY <= vpMinY + AUTOMATIC_OFFSET, vpMaxY - AUTOMATIC_OFFSET <= eY);
return new Pair<>(autoX, autoY);
}
private void dragTheNode(MouseEvent event, ScrollPane scroll) {
final double offsetX = event.getScreenX() - mouseX;
final double offsetY = event.getScreenY() - mouseY;
final double tX = x + offsetX;
final double tY = y + offsetY;
Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (tX >= 0 && tX <= (contentBounds.getWidth() - getWidth())) {
determineX(scroll, tX, event);
} else if (tX < 0) {
setLayoutX(0);
} else {
setLayoutX(contentBounds.getWidth() - getWidth());
}
if (tY >= 0 && tY <= (contentBounds.getHeight() - getHeight())) {
determineY(scroll, tY, event);
} else if (tY < 0) {
setLayoutY(0);
} else {
setLayoutY(contentBounds.getHeight() - getHeight());
}
ensureVisible(scroll);
}
private boolean isMouseInDraggableViewPort(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
Bounds draggableBounds = new BoundingBox(viewportSceneBounds.getMinX() + AUTOMATIC_OFFSET,
viewportSceneBounds.getMinY() + AUTOMATIC_OFFSET,
viewportSceneBounds.getWidth() - (2 * AUTOMATIC_OFFSET),
viewportSceneBounds.getHeight() - (2 * AUTOMATIC_OFFSET));
return draggableBounds.contains(event.getSceneX(), event.getSceneY());
}
private void determineY(ScrollPane scrollPane, double tY, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
final double visibleYInViewport = vpMinY + (vpHeight - getHeight());
if (tY >= vpMinY && tY <= visibleYInViewport) {
setLayoutY(tY);
} else if (tY < vpMinY) {
setLayoutY(vpMinY);
} else {
setLayoutY(visibleYInViewport);
}
}
private void determineX(ScrollPane scrollPane, double tX, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final double visibleXInViewport = vpMinX + (vpWidth - getHeight());
if (tX >= vpMinX && tX <= visibleXInViewport) {
setLayoutX(tX);
} else if (tX < vpMinX) {
setLayoutX(vpMinX);
} else {
setLayoutX(visibleXInViewport);
}
}
}
我有可以移动的节点,这些节点放置在 ScrollPane.When 上的窗格上 我将一个节点拖到滚动窗格的 viewportBounds
之外, vvalue
应该改变,以便节点再次在这些范围内。为了解决这个问题,我尝试使用 this question.
我的问题是当节点再次在viewportBounds
的边界内后,光标相对于节点移动,如果我想继续将节点移动到视口之外,经过几次迭代光标会移动太多以至于它会超出整个应用程序 window 并且会靠在屏幕边界上。如何保持光标在节点上的位置?
如果您想测试代码,请记住 viewport
边界的重构仅在您沿 Y 轴移动节点时发生。
public class NewFXMain extends Application {
@Override
public void start(Stage primaryStage) {
AnchorPane root = new AnchorPane();
ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000,5000);
Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
DraggableNode node = new DraggableNode(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node.setStyle(
"-fx-background-color: #334488; "
+ "-fx-text-fill: black; "
+ "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class DraggableNode extends Pane {
private double x = 0;
private double y = 0;
private double mousex = 0;
private double mousey = 0;
private Node view;
private boolean dragging = false;
private boolean moveToFront = true;
private void ensureVisible(ScrollPane scrollPane) {
Bounds viewport = scrollPane.getViewportBounds();
double contentHeight = scrollPane.getContent().localToScene(scrollPane.getContent().getBoundsInLocal()).getHeight();
double nodeMinY = this.localToScene(this.getBoundsInLocal()).getMinY();
double nodeMaxY = this.localToScene(this.getBoundsInLocal()).getCenterY();
double vValueDelta = 0;
double vValueCurrent = scrollPane.getVvalue();
if (nodeMaxY < 0) {
// currently located above (remember, top left is (0,0))
vValueDelta = (nodeMinY) / contentHeight;
System.out.println("FIRST CASE DELTA: " + vValueDelta);
} else if (nodeMinY > viewport.getHeight()) {
// currently located below
vValueDelta = ((nodeMinY) / contentHeight) / 5;
System.out.println("SECOND CASE DELTA: " + vValueDelta);
}
scrollPane.setVvalue(vValueCurrent + vValueDelta);
}
public DraggableNode(ScrollPane pane) {
init(pane);
}
private void init(ScrollPane scroll) {
onMousePressedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// record the current mouse X and Y position on Node
mousex = event.getSceneX();
mousey = event.getSceneY();
x = getLayoutX();
y = getLayoutY();
if (isMoveToFront()) {
toFront();
}
}
});
//Event Listener for MouseDragged
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
// Get the exact moved X and Y
double offsetX = event.getSceneX() - mousex;
double offsetY = event.getSceneY() - mousey;
x += offsetX;
y += offsetY;
double scaledX = x;
double scaledY = y;
setLayoutX(scaledX);
setLayoutY(scaledY);
ensureVisible(scroll);
dragging = true;
// again set current Mouse x AND y position
mousex = event.getSceneX();
mousey = event.getSceneY();
event.consume();
}
});
onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
dragging = false;
}
});
}
public void setMoveToFront(boolean moveToFront) {
this.moveToFront = moveToFront;
}
public boolean isMoveToFront() {
return moveToFront;
}
}
回答你的问题:
How do I maintain position of cursor on the node?
您需要使用 AWT Robot class 来移动光标。
private void movePointer(Node node){
Bounds b = node.localToScreen(node.getLayoutBounds());
try {
final Robot robot = new Robot();
robot.mouseMove((int) (b.getMinX()+(b.getWidth()/2)), (int) (b.getMinY()+(b.getHeight()/2)));
} catch (final AWTException e) {
System.out.println("Unable to poistion the pointer");
}
}
上述方法会将光标定位在提供的节点的中心。设置vValue后即可调用上述方法。
话虽如此,从用户的角度来看,这是非常奇怪的行为
- 让节点超出视口边界然后跳回 突然让它可见
- 跳转光标
所以我尝试以一种方式实现,拖动的节点 永远不会 超出视口边界并且 vValue/hValue 根据拖动调整并维护节点在视口中可见。
为了解决光标到达 app/screen 边界的另一个问题,我实现了一种在拖动时自动开始滚动的方法:一旦光标超出视口 bounds.When,光标就会返回视口边界,它只是作为一个正常的拖动操作。
下面是具有所需更改的代码的快速演示。您可以根据自己的需要进行微调。
我在窗格中添加了背景以展示正在进行的拖动操作;)
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.Pair;
public class NewFXMainV1 extends Application {
public static void main(final String[] args) {
launch(args);
}
@Override
public void start(final Stage primaryStage) {
final AnchorPane root = new AnchorPane();
// Adding a pattern to background to observe the automatic drag :)
String bg1 = "linear-gradient(from 0% 0% to 1% 0% , repeat, #DDDDDD 50% , transparent 22% )";
String bg2 = "linear-gradient(from 0% 0% to 0% 1% , repeat, transparent 50% , #DDDDDD 22% )";
root.setStyle("-fx-background-color:" + bg1 + ", " + bg2 + ";");
final ScrollPane scrollPane = new ScrollPane(root);
root.setPrefSize(5000, 5000);
final Scene scene = new Scene(scrollPane, 800, 600, Color.rgb(160, 160, 160));
final int numNodes = 6; // number of nodes to add
final double spacing = 30; // spacing between nodes
// add numNodes instances of DraggableNode to the root pane
for (int i = 0; i < numNodes; i++) {
final DraggableNodeV1 node = new DraggableNodeV1(scrollPane);
node.setPrefSize(98, 80);
// define the style via css
node
.setStyle(
"-fx-background-color: #334488; " + "-fx-text-fill: black; " + "-fx-border-color: black;");
node.setLayoutX(spacing * (i + 1) + node.getPrefWidth() * i);// position the node
node.setLayoutY(spacing);
root.getChildren().add(node); // add the node to the root pane
}
// finally, show the stage
primaryStage.setTitle("Draggable Node 01");
primaryStage.setScene(scene);
primaryStage.show();
}
}
class DraggableNodeV1 extends Pane {
private static Duration d = Duration.millis(26);
private static double PX_JUMP = 10;
private static double AUTOMATIC_OFFSET = 15;
private double x = 0;
private double y = 0;
private double mouseX = 0;
private double mouseY = 0;
private Timeline xTimeline;
private Timeline yTimeline;
public DraggableNodeV1(final ScrollPane pane) {
init(pane);
}
private void ensureVisible(final ScrollPane scrollPane) {
final Pane dragNode = this;
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final Bounds contentBounds = scrollPane.getContent().getLayoutBounds();
final double layoutY = dragNode.getLayoutY();
final double layoutX = dragNode.getLayoutX();
final double visibleYInViewport = vpMinY + (vpHeight - dragNode.getHeight());
double totalHeightToScroll = contentBounds.getHeight() - vpHeight;
if (layoutY > visibleYInViewport) {
double heightToScroll = vpMinY + (layoutY - visibleYInViewport);
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
} else if (layoutY < vpMinY) {
double heightToScroll = layoutY;
scrollPane.setVvalue(heightToScroll / totalHeightToScroll);
}
final double visibleXInViewport = vpMinX + (vpWidth - dragNode.getWidth());
double totalWidthToScroll = contentBounds.getWidth() - vpWidth;
if (layoutX > visibleXInViewport) {
double widthToScroll = vpMinX + (layoutX - visibleXInViewport);
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
} else if (layoutX < vpMinX) {
double widthToScroll = layoutX;
scrollPane.setHvalue(widthToScroll / totalWidthToScroll);
}
}
private void init(final ScrollPane scroll) {
onMousePressedProperty().set(event -> {
mouseX = event.getScreenX();
mouseY = event.getScreenY();
x = getLayoutX();
y = getLayoutY();
});
// Event Listener for MouseDragged
onMouseDraggedProperty().set(event -> {
if (isMouseInDraggableViewPort(event, scroll)) {
dragTheNode(event, scroll);
clearTimelines();
} else {
Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> auto = isMouseInAutomaticDrag(event, scroll);
Pair<Boolean, Boolean> xAuto = auto.getKey();
if (xAuto.getKey() || xAuto.getValue()) {
if (xAuto.getKey()) { // towards left
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutX() > 0) {
setLayoutX(getLayoutX() - PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
} else if (xAuto.getValue()) { // towards right
if (xTimeline == null) {
xTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutX() < contentBounds.getWidth() - getWidth()) {
setLayoutX(getLayoutX() + PX_JUMP);
x = getLayoutX();
mouseX = event.getScreenX();
ensureVisible(scroll);
}
}));
xTimeline.setCycleCount(Animation.INDEFINITE);
xTimeline.play();
}
}
} else {
stopXTimeline();
}
Pair<Boolean, Boolean> yAuto = auto.getValue();
if (yAuto.getKey() || yAuto.getValue()) {
if (yAuto.getKey()) { // towards top
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
if (getLayoutY() > 0) {
setLayoutY(getLayoutY() - PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
} else if (yAuto.getValue()) { // towards bottom
if (yTimeline == null) {
yTimeline = new Timeline(new KeyFrame(d, e -> {
final Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (getLayoutY() < contentBounds.getHeight() - getHeight()) {
setLayoutY(getLayoutY() + PX_JUMP);
y = getLayoutY();
mouseY = event.getScreenY();
ensureVisible(scroll);
}
}));
yTimeline.setCycleCount(Animation.INDEFINITE);
yTimeline.play();
}
}
} else {
stopYTimeline();
}
}
event.consume();
});
onMouseReleasedProperty().set(event -> clearTimelines());
}
private void clearTimelines() {
stopXTimeline();
stopYTimeline();
}
private void stopXTimeline() {
if (xTimeline != null) {
xTimeline.stop();
}
xTimeline = null;
}
private void stopYTimeline() {
if (yTimeline != null) {
yTimeline.stop();
}
yTimeline = null;
}
private Pair<Pair<Boolean, Boolean>, Pair<Boolean, Boolean>> isMouseInAutomaticDrag(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
double eX = event.getSceneX();
double eY = event.getSceneY();
double vpMinX = viewportSceneBounds.getMinX();
double vpMaxX = viewportSceneBounds.getMaxX();
double vpMinY = viewportSceneBounds.getMinY();
double vpMaxY = viewportSceneBounds.getMaxY();
Pair<Boolean, Boolean> autoX = new Pair<>(eX <= vpMinX + AUTOMATIC_OFFSET, vpMaxX - AUTOMATIC_OFFSET <= eX);
Pair<Boolean, Boolean> autoY = new Pair<>(eY <= vpMinY + AUTOMATIC_OFFSET, vpMaxY - AUTOMATIC_OFFSET <= eY);
return new Pair<>(autoX, autoY);
}
private void dragTheNode(MouseEvent event, ScrollPane scroll) {
final double offsetX = event.getScreenX() - mouseX;
final double offsetY = event.getScreenY() - mouseY;
final double tX = x + offsetX;
final double tY = y + offsetY;
Bounds contentBounds = scroll.getContent().getLayoutBounds();
if (tX >= 0 && tX <= (contentBounds.getWidth() - getWidth())) {
determineX(scroll, tX, event);
} else if (tX < 0) {
setLayoutX(0);
} else {
setLayoutX(contentBounds.getWidth() - getWidth());
}
if (tY >= 0 && tY <= (contentBounds.getHeight() - getHeight())) {
determineY(scroll, tY, event);
} else if (tY < 0) {
setLayoutY(0);
} else {
setLayoutY(contentBounds.getHeight() - getHeight());
}
ensureVisible(scroll);
}
private boolean isMouseInDraggableViewPort(MouseEvent event, ScrollPane scrollPane) {
Node viewport = scrollPane.lookup(".viewport");
Bounds viewportSceneBounds = viewport.localToScene(viewport.getLayoutBounds());
Bounds draggableBounds = new BoundingBox(viewportSceneBounds.getMinX() + AUTOMATIC_OFFSET,
viewportSceneBounds.getMinY() + AUTOMATIC_OFFSET,
viewportSceneBounds.getWidth() - (2 * AUTOMATIC_OFFSET),
viewportSceneBounds.getHeight() - (2 * AUTOMATIC_OFFSET));
return draggableBounds.contains(event.getSceneX(), event.getSceneY());
}
private void determineY(ScrollPane scrollPane, double tY, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinY = viewport.getMinY() * -1;
double vpHeight = viewport.getHeight();
final double visibleYInViewport = vpMinY + (vpHeight - getHeight());
if (tY >= vpMinY && tY <= visibleYInViewport) {
setLayoutY(tY);
} else if (tY < vpMinY) {
setLayoutY(vpMinY);
} else {
setLayoutY(visibleYInViewport);
}
}
private void determineX(ScrollPane scrollPane, double tX, MouseEvent event) {
final Bounds viewport = scrollPane.getViewportBounds();
double vpMinX = viewport.getMinX() * -1;
double vpWidth = viewport.getWidth();
final double visibleXInViewport = vpMinX + (vpWidth - getHeight());
if (tX >= vpMinX && tX <= visibleXInViewport) {
setLayoutX(tX);
} else if (tX < vpMinX) {
setLayoutX(vpMinX);
} else {
setLayoutX(visibleXInViewport);
}
}
}