Search code examples
javajavafxjavafx-8zoomingscrollpane

JavaFx 8 - Scaling / zooming ScrollPane relative to mouse position


I need to zoom in / out on a scroll pane, relative to the mouse position.

I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position)

This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled.

My code:

@Override
public void initialize(URL location, ResourceBundle resources) {

    Delta initial_mouse_pos = new Delta();

    anchorpane.setOnScrollStarted(event -> {
        initial_mouse_pos.x = event.getX();
        initial_mouse_pos.y = event.getY();
    });

    anchorpane.setOnScroll(event -> {
        double zoom_fac = 1.05;
        double delta_y = event.getDeltaY();

        if(delta_y < 0) {
            zoom_fac = 2.0 - zoom_fac;
        }

        Scale newScale = new Scale();
        newScale.setPivotX(initial_mouse_pos.x);
        newScale.setPivotY(initial_mouse_pos.y);
        newScale.setX( content_group.getScaleX() * zoom_fac );
        newScale.setY( content_group.getScaleY() * zoom_fac );

        content_group.getTransforms().add(newScale);

        event.consume();
    });
}

private class Delta { double x, y; }

How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?


Solution

  • This is a scalable, pannable JavaFX ScrollPane :

    import javafx.geometry.Bounds;
    import javafx.geometry.Point2D;
    import javafx.geometry.Pos;
    import javafx.scene.Group;
    import javafx.scene.Node;
    import javafx.scene.control.ScrollPane;
    import javafx.scene.layout.VBox;
    
    public class ZoomableScrollPane extends ScrollPane {
        private double scaleValue = 0.7;
        private double zoomIntensity = 0.02;
        private Node target;
        private Node zoomNode;
    
        public ZoomableScrollPane(Node target) {
            super();
            this.target = target;
            this.zoomNode = new Group(target);
            setContent(outerNode(zoomNode));
    
            setPannable(true);
            setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
            setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
            setFitToHeight(true); //center
            setFitToWidth(true); //center
    
            updateScale();
        }
    
        private Node outerNode(Node node) {
            Node outerNode = centeredNode(node);
            outerNode.setOnScroll(e -> {
                e.consume();
                onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
            });
            return outerNode;
        }
    
        private Node centeredNode(Node node) {
            VBox vBox = new VBox(node);
            vBox.setAlignment(Pos.CENTER);
            return vBox;
        }
    
        private void updateScale() {
            target.setScaleX(scaleValue);
            target.setScaleY(scaleValue);
        }
    
        private void onScroll(double wheelDelta, Point2D mousePoint) {
            double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
    
            Bounds innerBounds = zoomNode.getLayoutBounds();
            Bounds viewportBounds = getViewportBounds();
    
            // calculate pixel offsets from [0, 1] range
            double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
            double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
    
            scaleValue = scaleValue * zoomFactor;
            updateScale();
            this.layout(); // refresh ScrollPane scroll positions & target bounds
    
            // convert target coordinates to zoomTarget coordinates
            Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
    
            // calculate adjustment of scroll position (pixels)
            Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
    
            // convert back to [0, 1] range
            // (too large/small values are automatically corrected by ScrollPane)
            Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
            this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
            this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
        }
    }