Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a JavaFX Group not move around when just adding children?

Tags:

javafx-2

If you watch the following test, you'll see that all the circles move around instead of just a new one being added. It doesn't happen every time. I think it's only when the new child is outside the existing bounds. But how do I get it so that it will not move the group and all it's children when I add another circle, regardless of where I put the circle?

Note, that if I don't set the scale on the Group, they won't all move. So it's related to setting the scale.

import javafx.application.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.stage.*;

import java.util.*;


public class GroupTest extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    public void start(Stage stage) {
        Pane pane = new Pane();

        Group root = new Group();
        // NOTE: removing these two setScale* lines stops the undesirable behavior
        root.setScaleX(.2); 
        root.setScaleY(.2);
        root.setTranslateX(100);
        root.setTranslateY(100);

        root.layoutXProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observableValue, Number number, Number number2) {
                System.out.println("root layout: " + root.getLayoutX() + ", " + root.getLayoutY());
            }
        });

        root.getChildren().addListener(new ListChangeListener<Node>() {
            @Override public void onChanged(Change<? extends Node> change) {
                System.out.println("root: " + root.getBoundsInParent());
                System.out.println("root: " + root.getBoundsInLocal());
                System.out.println("root: " + root.getLayoutBounds());
                System.out.println("root: " + root.getLayoutX() + ", " + root.getLayoutY());
            }
        });

        pane.getChildren().add(root);
        Scene scene = new Scene(pane, 500, 500);
        stage.setScene(scene);
        stage.show();

        new Thread(() -> {
            Random r = new Random();
            try {
                while (true) {
                    expand = expand * 1.1;
                    Thread.sleep(700);
                    Platform.runLater(() -> {
                        root.getChildren().add(new Circle(r.nextInt((int)(1000*expand)) - 500*expand, r.nextInt((int)(1000*expand)) - 500*expand, r.nextInt(50)+30));
                    });
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    static double expand = 1.0;
}
like image 382
mentics Avatar asked Jul 02 '13 14:07

mentics


2 Answers

First, I want to say that the behavior you see can be achieved through a much smaller program, let alone those calculations you do for the circles. r.nextInt(250) for the positions of the circles would have been enough to see the behavior and is much easier to see what happens. Also, for debugging, I added a visible rectangle to the pane that is bound to the Group's layoutbounds, where you can see what happens:

final Rectangle background = new Rectangle(0, 0, 0, 0);

    root.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
      @Override
      public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
        background.setX(newValue.getMinX());
        background.setY(newValue.getMinY());
        background.setWidth(newValue.getWidth());
        background.setHeight(newValue.getHeight());
      }
    });
    background.setFill(null);
    background.setStroke(Color.RED);
    pane.getChildren().add(background);

So, what happens here?

From the Group's API:

Any transform, effect, or state applied to a Group will be applied to all children of that group. Such transforms and effects will NOT be included in this Group's layout bounds, however if transforms and effects are set directly on children of this Group, those will be included in this Group's layout bounds.

Looking at the result with your scale turned on:

enter image description here

You see that the bounds of the group are larger than whats inside. This is because of how the transformation is applied: The children of the group are transformed, but for the calculation of the bounds of the group the scaling is not considered. Thus, the group is on the pane where the union of the untransformed bounds of the circles are, then transformations for the circles are applied.

Compare with this statement with the result when the scaling is turned off:

enter image description here

To sum up, this is by design and not a weird behavior, because the Group is always as big and positioned accordingly where the union of its untransformed children bounds are.

EDIT

If you want the nodes to be scaled at the position they are and the group not move, I suggest to scale the children of the group directly. This implementation of your thread changes the scaling of the circles every 5 circles, but they stay at the same position:

new Thread(new Runnable() {
      private int count = 0;
      private double scale1 = .5;
      private double scale2 = .2;
      private double currentScale = scale1;
      @Override
      public void run() {
        final Random r = new Random();
        try {
          while (true) {
            expand = expand * 1.1;
            Thread.sleep(700);
            Platform.runLater(new Runnable() {
              @Override
              public void run() {
                System.out.println(count);
                Circle c = new Circle(r.nextInt(250), r.nextInt(250), 30);
                c.setScaleX(currentScale);
                c.setScaleY(currentScale);
                root.getChildren().add(c);
                count++;
                if (count > 5){
                  count = 0;
                  if (currentScale == scale1){
                    currentScale = scale2;
                  } else {
                    currentScale = scale1;
                  }
                  Iterator<Node> iterator = root.getChildren().iterator();
                  while (iterator.hasNext()) {
                    Node next = iterator.next();
                    next.setScaleX(currentScale);
                    next.setScaleY(currentScale);
                  }
                }
              }
            });
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }).start();
like image 96
zhujik Avatar answered Sep 28 '22 07:09

zhujik


The real problem here is that the Group's pivot point -- the relative point for rotation and scaling -- is dependent on the Group's layout-bounds. Therefore, if layout-bounds change, due to adding new children (or existent children changing position, size, or rotation), the relative point for all transformations on the Group changes as well.

In the JavaFX source code, you can find the definition of the pivot point in the Node.java file. The methods impl_getPivotX() and impl_getPivotY() return the center x resp. y coordinate of the layout-bounds.

Unfortunately, there is no option to manually set the pivot point. But as every Node manages a list of additional transformations which are applied on top of the standard transformations, you can easily achieve the desired behaviour:

final Scale scale = new Scale();
group.getTransforms().add(scale);

// change scale
scale.setX(2);
like image 41
Matthias Avatar answered Sep 28 '22 08:09

Matthias