Mittwoch, 13. November 2013

Scaling vs Zooming in JavaFx

JavaFx has a built in scaling functionality. We will see in a few lines, what that means. I needed a zooming functionality like you find it in Firefox. You can make the content e.g. bigger, but that means, that all content is still painted. If the window becomes too small, you get scrollbars, but nothing of the content is lost.

Scaling in JavaFx

Every node in the scene graph in JavaFx has a the ability to scale it's content. But that does NOT effect the layouting behaviour of this node. Here we have a simple exampe with a webview and a slider on the bottom. With the slider you can change the scaling from 0.5 to 2.0. As you can see, if the zoom factor is smaller than 1, there is a white border round our webview, and if the zoom factor is bigger than 1, not all content is shown


How to zoom right?

I didn't find a fitting example on the web, so I tried it several times, to do it right. And the only way, I know is to create your own Pane implementation to connect the scaling and the layout, which must fit together to do a zoom. Here is the code example:


As you can see, we intruduces the class ZoomingPane, that subclasses Pane. There are two important things to do:

Implement a customized layout

To implement an individual layout you must overload the layoutChildren method. The code insight is mainyl copied from StackPane with one main difference: The computing of the contentWidth and the contentHeight uses the zoomFactor.


Adding a Scale transformation

Apply a Scale transformation which is connected to the zoomFactor Property. In our example this ist done in the constructor. We create a new Scale with the factors 1 and 1, which means no zooming at all. The Scale instance is applied to the content. Then we add a ChangeListener to the zoomFactor property, which takes the new zoomFactor, applies it to the Scale transsformation and calls requestLayout() which is important to tell JavaFx to relayout.

You may ask, why I didn't bind the x and y property of the scale to the zoomFactor. The reason is, that we still need the listener to request the new layout, so don't have any advantage.

What we didn't cover in this post is the computation of the preferred, the minimum and the maximum size of our new pane. If you use that ZoomingPane as root node in the scene as I do, this is not necessary.

1 Kommentar:

  1. Good hint. Thank you for the code.

    But I won't use a constructor with the new child as an Argument. I use JavaFX, and JavaFX doesn't allow it. So i'd propose the changes below. Each time the children get changed, they receive the new scale and change their transform value.


    package myexplorer.mywidgets;

    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.ListChangeListener;
    import javafx.geometry.Pos;
    import javafx.scene.layout.Pane;
    import javafx.scene.transform.Scale;

    public class ZoomingPane extends Pane {

    // private final DoubleProperty zoomFactor = new SimpleDoubleProperty(1);
    private final DoubleProperty zoomProzent = new SimpleDoubleProperty(100.0d);

    public ZoomingPane() {
    super();
    this.setScaleX(1.0d);
    this.setScaleY(1.0d);

    getChildren().addListener(new ListChangeListener() {
    @Override
    public void onChanged(ListChangeListener.Change change) {
    getChildren().forEach(
    child -> child.getTransforms().add(getScale()));
    }
    });

    zoomProzent.addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue observable,
    Number oldValue,
    Number newValue) {
    setScaleX(newValue.doubleValue() / 100);
    setScaleY(newValue.doubleValue() / 100);
    requestLayout();
    }
    });
    }

    @Override
    protected void layoutChildren() {
    Pos pos = Pos.TOP_LEFT;
    double width = getWidth();
    double height = getHeight();
    double top = getInsets().getTop();
    double right = getInsets().getRight();
    double left = getInsets().getLeft();
    double bottom = getInsets().getBottom();
    double zoomFactor = zoomProzent.get() / 100;
    double contentWidth = (width - left - right) / zoomFactor;
    double contentHeight = (height - top - bottom) / zoomFactor;
    getChildren().forEach(
    child -> layoutInArea(child, left, top, contentWidth, contentHeight,
    0, null, pos.getHpos(), pos.getVpos()));
    }

    public Scale getScale() {
    Scale scale = new Scale(this.getScaleX(), this.getScaleY());
    return scale;
    }

    public final Double getZoomProzent() {
    return zoomProzent.get();
    }

    public final void setZoomProzent(Double zoomProzent) {
    this.zoomProzent.set(zoomProzent);
    }

    public final DoubleProperty zoomProzentProperty() {
    return zoomProzent;
    }
    }

    AntwortenLöschen