简体   繁体   中英

JavaFX ScrollPane update of viewportBoundsProperty

I do not understand how javaFX's ScrollPane control updates the viewportBounds property:

  • when drag and dropping to pan the property is updated
  • when using the horizontal and vertical bars or arrows to pan, the property is not updated.

Is there a javaFX expert out there that could explain to me how this property of ScrollPane works and how to use it to recompute the content of this scroll pane? (I have a dynamc tile map which is just loaded if visible).

Here is a minimal example illustrating this behaviour:

public class DemoScrollPane extends javafx.application.Application {
    private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);


    public static void run(String[] args) {
        javafx.application.Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // create content
        Pane pane = new Pane();
        Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
        Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
        pane.getChildren().add(rectangle);
        pane.getChildren().add(rectangleInitialVP);

        // create scrollPane
        ScrollPane scrollPane = new ScrollPane();
        scrollPane.setContent(pane);
        // add click and drag interaction
        scrollPane.setPannable(true);

        // register a watcher on scrollPane viewportBoundsProperty
        InvalidationListener listener =
                    o -> {
                        logger.debug("viewport change {}",scrollPane.getViewportBounds());
                    };
        // This is triggered when moving using click and drag (pan)
        // and does correctly update the scrollPane viewportbounds.
        
        // BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac) 
        // without clicking.
        // (even tough the viewport obviously change)
        scrollPane.viewportBoundsProperty().addListener(listener);

        // display the app
        Scene scene = new Scene(scrollPane, 640, 480);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        logger.debug("stop");
    }
}

Here is also a video that shows how certain panning or interactions will not generate viewportBoundsProperty updates and click and drag panning does: video-of-scrollpane-no-update-of-viewportBoundsProperty

Thanks

Thanks to @James_D's comment here is a solution that goes around the problem (see below).

If someone has a better understanding of JavaFX's ScrollPane do not hesitate to explain difference between

  • the click and scroll that updates the viewPort of the ScrollPane
  • vs scroll with H and V bars that does not update the viewPort of the ScrollPane .

Here is the solution taken from from James_D's comment:

public class DemoScrollPane extends javafx.application.Application {
    private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);


    public static void run(String[] args) {
        javafx.application.Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // create content
        Pane pane = new Pane();
        pane.setMaxSize(1000,1000);
        Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
        Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
        pane.getChildren().add(rectangle);
        pane.getChildren().add(rectangleInitialVP);


        // create scrollPane
        ScrollPane scrollPane = new ScrollPane();
        scrollPane.setContent(pane);
        scrollPane.setVmin(0);
        scrollPane.setVmax(pane.getMaxHeight());
        scrollPane.setHmin(0);
        scrollPane.setHmax(pane.getMaxWidth());
        
        // add click and drag interaction
        scrollPane.setPannable(true);

        // register a watcher on scrollPane viewportBoundsProperty
        InvalidationListener listener =
                    o -> {
            Bounds movingViewPort = new BoundingBox(scrollPane.getHvalue(),
                    scrollPane.getVvalue(),
                    0.,
                    scrollPane.getViewportBounds().getWidth(),
                    scrollPane.getViewportBounds().getHeight(),
                    0.
                    );
                        logger.debug("viewport {}",movingViewPort);
                    };
        // This is triggered when moving using click and drag (pan)
        // and does correctly update the scrollPane viewportbounds.

        // BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac)
        // without clicking.
        // (even tough the viewport obviously change)
        scrollPane.vvalueProperty().addListener(listener);
        scrollPane.hvalueProperty().addListener(listener);

        // display the app
        Scene scene = new Scene(scrollPane, 640, 480);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        logger.debug("stop");
    }
}

The simple answer of your question about the difference is:: that's how it is implemented !!

Firstly, the viewPort bounds are updated everytime the scrollPane tries to layout its children. The sneak peak of layoutChildren method in ScrollPaneSkin class is as below:

protected void layoutChildren(double x, double y, double w, double h) {
  ScrollPane control = (ScrollPane)this.getSkinnable();
  ... // All the stuff for layouting
  ...
  control.setViewportBounds(new BoundingBox(this.snapPosition(this.viewContent.getLayoutX()), this.snapPosition(this.viewContent.getLayoutY()), this.snapSize(this.contentWidth), this.snapSize(this.contentHeight)));
} 

Now if we check for the related implementation for "pannable" property of ScrollPane, the code is as below (in ScrollPaneSkin class):

this.viewRect.setOnDragDetected((e) -> {
    if (IS_TOUCH_SUPPORTED) {
        this.startSBReleasedAnimation();
    }

    if (((ScrollPane)this.getSkinnable()).isPannable()) {
        this.dragDetected = true;
        if (this.saveCursor == null) {
            this.saveCursor = ((ScrollPane)this.getSkinnable()).getCursor();
            if (this.saveCursor == null) {
                this.saveCursor = Cursor.DEFAULT;
            }

            ((ScrollPane)this.getSkinnable()).setCursor(Cursor.MOVE);
            ((ScrollPane)this.getSkinnable()).requestLayout(); // !! THIS LINE !!
        }
    }

});
this.viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, (e) -> {
    this.mouseDown = false;
    if (this.dragDetected) {
        if (this.saveCursor != null) {
            ((ScrollPane)this.getSkinnable()).setCursor(this.saveCursor);
            this.saveCursor = null;
            ((ScrollPane)this.getSkinnable()).requestLayout();// !! THIS LINE !!
        }

        this.dragDetected = false;
    }

    if ((this.posY > ((ScrollPane)this.getSkinnable()).getVmax() || this.posY < ((ScrollPane)this.getSkinnable()).getVmin() || this.posX > ((ScrollPane)this.getSkinnable()).getHmax() || this.posX < ((ScrollPane)this.getSkinnable()).getHmin()) && !this.touchDetected) {
        this.startContentsToViewport();
    }

});

From the above code you can notice that, when the "pannable" property is true, if a drag is detected, it will request a layout. And in the next scene pulse it will call the layoutChildren and will update the viewportBounds. And the same thing happens when you release the mouse (if a drag is detected). That is the reason you can see the logs only at the start and end of the panning and not while dragging.

Now coming to the code of hbar/vbar dragging, the code when the scrollBar value is updated is as below: (in ScrollPaneSkin class)

InvalidationListener vsbListener = (valueModel) -> {
    if (!IS_TOUCH_SUPPORTED) {
        this.posY = Utils.clamp(((ScrollPane)this.getSkinnable()).getVmin(), this.vsb.getValue(), ((ScrollPane)this.getSkinnable()).getVmax());
    } else {
        this.posY = this.vsb.getValue();
    }

    this.updatePosY();
};
this.vsb.valueProperty().addListener(vsbListener);
InvalidationListener hsbListener = (valueModel) -> {
    if (!IS_TOUCH_SUPPORTED) {
        this.posX = Utils.clamp(((ScrollPane)this.getSkinnable()).getHmin(), this.hsb.getValue(), ((ScrollPane)this.getSkinnable()).getHmax());
    } else {
        this.posX = this.hsb.getValue();
    }

    this.updatePosX();
};
this.hsb.valueProperty().addListener(hsbListener);  

The value listeners just update the layoutX/Y property of the viewContent but not a layout request. The updatePosX method is as below:

private double updatePosX() {
    ScrollPane sp = (ScrollPane)this.getSkinnable();
    double x = this.isReverseNodeOrientation() ? this.hsb.getMax() - (this.posX - this.hsb.getMin()) : this.posX;
    double minX = Math.min(-x / (this.hsb.getMax() - this.hsb.getMin()) * (this.nodeWidth - this.contentWidth), 0.0D);
    this.viewContent.setLayoutX(this.snapPosition(minX));
    if (!sp.hvalueProperty().isBound()) {
        sp.setHvalue(Utils.clamp(sp.getHmin(), this.posX, sp.getHmax()));
    }

    return this.posX;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM