I am trying to create a class which handles zooming and panning components. The class contains references to an Anchorpane, which I am using since it does not do any aligning by default, as well as an ImageView which I want to be able to drag and zoom onto. The zooming and panning both work individually, but I am having trouble re-centering the image once it has been zoomed in or out.
The idea is that I want to be able to recenter the image when the app resizes so that dragging always appears to be relative to the center of the Anchorpane. By default any displacement in an Anchorpane is relative to the top-left corner, but that is not intuitive to the user. It would be much more logical for the content to appear to be moving relative to the center. To achieve this, the idea is to recenter the content whenever the window changes size, and then apply a translation corresponding to the amount of dragging done by the user
If you run the code I posted and zoom in, then resize the window, you'll notice that the red rectangle representing the image goes all over the place. If you shrink the window, then the square will no longer be at the same place as before. This only happens when scaling has been applied and appear to be a problem with the re-centering method.
If the centering method worked correctly, the square should return to the same position is was at before the window was expanded, which happens when the square is at 1 to 1 scale and no zooming has occured
Here is the class I use to handle the zoom and dragging
package com.example.test;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
public class ZoomController{
// the plane on which the node is being dragged
private final AnchorPane PLANE;
// the node I want to zoom and drag
private final Node CONTENT;
private Point2D lastContentPosition = new Point2D(0, 0);
private Point2D lastMousePosition = new Point2D(0, 0);
// the total amount of dragging
// applied to the CONTENT node
private Point2D dragOffset = new Point2D(0, 0);
// the total amount of scaling
// applied to the CONTENT node
private int scale = 1;
public ZoomController(
AnchorPane plane,
Node content
) {
this.PLANE = plane;
this.CONTENT = content;
addListeners();
// artificially reproduces the problem
applyZoom(2, new Point2D(0, 350));
applyDrag(new Point2D(-100, 0));
}
private void addListeners() {
// tries to center the CONTENT whenever the window is resized
PLANE.heightProperty().addListener(centerContent());
PLANE.widthProperty().addListener(centerContent());
// saves the mouse's position whenever the mouse moves
PLANE.setOnMousePressed(getMousePosition());
PLANE.setOnMouseDragged(drag());
PLANE.setOnScroll(handleScroll());
}
private ChangeListener<Number> centerContent() {
return (observableValue, number, t1) -> {
centerView();
};
}
private EventHandler<MouseEvent> drag() {
return mouseEvent -> {
// calculates the path taken by the mouse...
Point2D newMousePosition = new Point2D(mouseEvent.getX(), mouseEvent.getY());
Point2D mouseTranslation = lastMousePosition.subtract(newMousePosition);
// ...and saves its new position
updateMousePosition(mouseEvent);
// applies the drag
applyDrag(mouseTranslation);
};
}
private EventHandler<MouseEvent> getMousePosition() {
return this::updateMousePosition;
}
private EventHandler<ScrollEvent> handleScroll() {
return scrollEvent -> {
// filters out the mouse stopping to scroll
if (scrollEvent.getDeltaX() == 0 && scrollEvent.getDeltaY() == 0) return;
// starts zooming
if (scrollEvent.isControlDown()) {
zoom(scrollEvent);
}
};
}
private void zoom(ScrollEvent scrollEvent) {
// adds or subtracts to the image's scale based on
// whether user is scrolling backwards or forwards
final double dScale = scrollEvent.getDeltaY() > 0 ? 0.1 : -0.1;
scale += dScale;
// gets the coordinates IN THE IMAGE's FRAME OF REFERENCE
// of the point at which to zoom the image so it is centered on the mouse
Point2D target = CONTENT.parentToLocal(new Point2D(scrollEvent.getX(), scrollEvent.getY()));
// applies the zoom to the image
applyZoom(1 + dScale, target);
// saves the image's position once it has been zoomed
updateContentPosition();
}
private void applyZoom(final double zoomAmount, Point2D target) {
// applies the necessary scaling to the image...
Scale zoom = new Scale(zoomAmount, zoomAmount);
// ...and centers the scaling to the point where the mouse is located at
zoom.setPivotY(target.getY());
zoom.setPivotX(target.getX());
CONTENT.getTransforms().add(zoom);
updateContentPosition();
}
private void applyDrag(Point2D dragAmount) {
// drag amount always corresponds to the mouse's displacement
// for the moment this is a 1 to 1 mapping
// since I have not figured out how to take the scale into consideration
// updates the total displacement caused by drag (used when we re-center the image)
dragOffset = dragOffset.subtract(dragAmount);
// applies the necessary translation to the image...
Translate drag = new Translate();
// ...based on the mouse's movement
drag.setX(-dragAmount.getX());
drag.setY(-dragAmount.getY());
CONTENT.getTransforms().add(drag);
// saves the image's position after it has been dragged
updateContentPosition();
}
private void centerView() {
// gets the coordinates we need to place the image at for it to be centered
Point2D centerPosition = getCenterEdge();
// calculates the path to take from the image's current position
// to the position it has to be at to be centered
// ie: the displacement vector
Point2D translation = centerPosition.subtract(lastContentPosition);
// applies the necessary translation to the image...
Translate translateToCenter = new Translate();
// ...while account for drag so image is not fully re-centered
translateToCenter.setX(translation.getX() + dragOffset.getX());
translateToCenter.setY(translation.getY() + dragOffset.getY());
CONTENT.getTransforms().add(translateToCenter);
// saves the image's position after it has been centered
updateContentPosition();
}
private void updateMousePosition(MouseEvent mouseEvent) {
lastMousePosition = new Point2D(mouseEvent.getX(), mouseEvent.getY());
}
private void updateContentPosition() {
// updates the image's position
lastContentPosition = getContentPosition();
}
private Point2D getContentPosition() {
// gets the minimal coordinates of the bounds around the image
// ie: the image's coordinates
Bounds contentBounds = CONTENT.getBoundsInParent();
return new Point2D(contentBounds.getMinX(), contentBounds.getMinY());
}
private Point2D getCenterEdge() {
// gets the size of the image and the anchor pane it is in...
Point2D contentSize = getContentSize();
Point2D availableSpace = getAvailableSpace();
// ...to determine the coordinates at which to place the image for it to be centerd
return new Point2D(
(availableSpace.getX() - contentSize.getX()) / 2,
(availableSpace.getY() - contentSize.getY()) / 2
);
}
private Point2D getContentSize() {
// gets the bounds around the image
Bounds contentBounds = CONTENT.getBoundsInParent();
return new Point2D(contentBounds.getWidth(), contentBounds.getHeight());
}
private Point2D getAvailableSpace() {
// gets the size of the Anchorpane the image is inn
return new Point2D(PLANE.getWidth(), PLANE.getHeight());
}
}
And here is the main class I am using to test the zoom
package com.example.test;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ZoomMain extends Application {
@Override
public void start(Stage stage) throws Exception {
// the node we want to drag & zoom
Rectangle rectangle = new Rectangle(200, 100);
rectangle.setFill(Color.RED);
// the plane on which the node is being dragged
AnchorPane plane = new AnchorPane();
// adds the node
Scene mainScene = new Scene(plane);
plane.getChildren().add(rectangle);
// handles the zoom
ZoomController zoomController = new ZoomController(
plane,
rectangle
);
stage.setTitle("Zooming test");
stage.setScene(mainScene);
stage.setMinHeight(500);
stage.setMinWidth(500);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have already tried to group the image to drag & zoom into a group, which would be useful since it would let me include multiple nodes to be dragged at once, however when I do that the contents of the group remain invisible.
Instead of re-centering the content after scale, use the center point of the content as the scale pivot.
In other words, scale around the center of the content.
Introduce a simple method to calculate the center:
private Point2D getContentCenter() {
Bounds contentBounds = CONTENT.getBoundsInLocal();
return new Point2D(contentBounds.getCenterX(), contentBounds.getCenterY());
}
And scale around it:
private void zoom(ScrollEvent scrollEvent) {
// adds or subtracts to the image's scale based on
// whether user is scrolling backwards or forwards
final double dScale = scrollEvent.getDeltaY() > 0 ? 0.1 : -0.1;
scale += dScale;
Point2D target = getContentCenter();
// applies the zoom to the image
applyZoom(1 + dScale, target);
}
EDIT:
The following ZoomController
implements scaling around mouth positions and recentering of the content when the pane is resized :
class ZoomController{
// the plane on which the node is being dragged
private final AnchorPane PLANE;
// the node I want to zoom and drag
private final Node CONTENT;
// the total amount of scaling applied to the CONTENT node
private int scale = 1;
private Point2D lastMousePosition = new Point2D(0, 0);
public ZoomController( AnchorPane plane, Node content ) {
PLANE = plane;
CONTENT = content;
PLANE.heightProperty().addListener((obs, number, t1) -> centerView());
PLANE.widthProperty().addListener((obs, number, t1) -> centerView());
// saves the mouse's position whenever the mouse moves
PLANE.setOnMousePressed(event -> updateMousePosition(event));
PLANE.setOnMouseDragged(drag());
PLANE.setOnScroll(handleScroll());
}
private void updateMousePosition(MouseEvent mouseEvent) {
lastMousePosition = new Point2D(mouseEvent.getX(), mouseEvent.getY());
}
private EventHandler<ScrollEvent> handleScroll() {
return scrollEvent -> {
// filters out the mouse stopping to scroll
if (scrollEvent.getDeltaX() == 0 && scrollEvent.getDeltaY() == 0) return;
// starts zooming
if (scrollEvent.isControlDown()) {
zoom(scrollEvent);
}
};
}
private void zoom(ScrollEvent scrollEvent) {
// adds or subtracts to the image's scale based on
// whether user is scrolling backwards or forwards
final double dScale = scrollEvent.getDeltaY() > 0 ? 0.1 : -0.1;
scale += dScale;
// scale around mouse position
Point2D pivot = CONTENT.parentToLocal(new Point2D(scrollEvent.getX(), scrollEvent.getY()));
//applies the zoom to the image
applyZoom(1 + dScale, pivot);
}
private void applyZoom(final double zoomAmount, Point2D target) {
// applies the necessary scaling to the image...
Scale zoom = new Scale(zoomAmount, zoomAmount);
// ...and centers the scaling to the point where the mouse is located at
zoom.setPivotY(target.getY());
zoom.setPivotX(target.getX());
CONTENT.getTransforms().add(zoom);
}
private EventHandler<MouseEvent> drag() {
return mouseEvent -> {
// calculates the path taken by the mouse...
Point2D newMousePosition = new Point2D(mouseEvent.getX(), mouseEvent.getY());
Point2D mouseTranslation = lastMousePosition.subtract(newMousePosition);
// ...and saves its new position
updateMousePosition(mouseEvent);
// applies the drag
applyDrag(mouseTranslation);
};
}
private void applyDrag(Point2D dragAmount) {
// applies the necessary translation to the image...
Translate drag = new Translate();
// ...based on the mouse's movement
drag.setX(-dragAmount.getX());
drag.setY(-dragAmount.getY());
CONTENT.getTransforms().add(drag);
}
private void centerView() {
// gets the coordinates we need to place the image at for it to be centered
Point2D centerPosition = CONTENT.parentToLocal(getCenterEdge());
Point2D position = getContentPosition();
Point2D translation = centerPosition.subtract(position);
// applies the necessary translation to the image...
Translate translateToCenter = new Translate();
translateToCenter.setX(translation.getX());
translateToCenter.setY(translation.getY());
CONTENT.getTransforms().add(translateToCenter);
}
private Point2D getContentPosition() {
// gets the minimal coordinates of the bounds around the image
// ie: the image's coordinates
Bounds contentBounds = CONTENT.getBoundsInLocal();
return new Point2D(contentBounds.getMinX(), contentBounds.getMinY());
}
private Point2D getCenterEdge() {
// gets the size of the image and the anchor pane it is in...
Point2D contentSize = getContentSize();
Point2D parentSize = getParentSize();
// ...to determine the coordinates at which to place the image for it to be centered
return new Point2D(
(parentSize.getX() - contentSize.getX()) / 2,
(parentSize.getY() - contentSize.getY()) / 2
);
}
private Point2D getContentSize() {
//BoundsInParent: rectangular bounds of this Node which include its transforms
Bounds contentBounds = CONTENT.getBoundsInParent();
return new Point2D(contentBounds.getWidth(), contentBounds.getHeight());
}
private Point2D getParentSize() {//renamed from getAvailableSpace()
// gets the size of the Anchorpane the image is inn
return new Point2D(PLANE.getWidth(), PLANE.getHeight());
}
}