javafx animation: displaying circles


Question

I want to display 5 randomly positioned and colored circles. It was easy part. Now I want to adjust this code to be an animation. This application should generate random circles endlessly but the condition is that it should keep only last five circles on the screen. This is where I got stuck. JavaFx provides ListChangeListener. I think it is what I should use. But how? The following is my unfinished code:

import java.util.Random;

import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class RandomColorTest extends Application {

    int radius = 20;
    int sceneWidth = 300;
    int sceneHeight = 300;

    private void init(Stage primaryStage) {
        Group root = new Group();
        primaryStage.setResizable(false);
        primaryStage.setScene(new Scene(root, sceneWidth,sceneHeight));

        for (int i = root.getChildren().size(); i < 5; i++) {
            root.getChildren().add(createCircle());
            // the following should convey the idea:
            // if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
            // add one new element 
            // if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
            // add one new element 
            // and so on
            root.getChildren().addListener(new ListChangeListener<E>() {
                @Override
                public void onChanged(
                        javafx.collections.ListChangeListener.Change<? extends E> arg0) {
                    // TODO Auto-generated method stub
                }
            });

        }
    }

    // Create randomly positioned and colored circle
    private Circle createCircle() {
        final Circle circle = new Circle();
        circle.setRadius(radius);

        Random r = new Random();
        int rCol1 = r.nextInt(256);
        int rCol2 = r.nextInt(256);
        int rCol3 = r.nextInt(256);
        int rX = radius+r.nextInt(sceneWidth);
        if (rX>sceneWidth-radius) {
            rX=rX-2*radius;
        }
        int rY = radius+r.nextInt(sceneHeight);
        if (rY>sceneHeight-radius) {
            rY=rY-2*radius;
        }
        circle.setLayoutX(rX);
        circle.setLayoutY(rY);

        circle.setStroke(Color.BLACK);
        circle.setFill(Color.rgb(rCol1,rCol2,rCol3));
        System.out.println(rCol1+"-"+rCol2+"-"+rCol3+"-"+rX+"-"+rY);
        return circle;
    }

    @Override public void start(Stage primaryStage) throws Exception {
        init(primaryStage);
        primaryStage.show();
    }
    public static void main(String[] args) { launch(args); }
}

After having managed to make ListChangeListener compile without errors it doesn't still work the way expected. Changes made to for loop:

for (int i = root.getChildren().size();;i++) {
            final ObservableList<Node> ol = root.getChildren();
            // the following should convey the idea:
            // if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
            // add one new element 
            // if the collection holds 5 elements then keep least recently generated element for 1 second and then delete it
            // add one new element 
            // and so on
            ol.add(createCircle());
            ol.addListener( new ListChangeListener<Node>(){
                @Override
                public void onChanged(
                    javafx.collections.ListChangeListener.Change<? extends Node> arg0) {
                    // TODO Auto-generated method stub
                    System.out.println("one new element added, size:"+ol.size());
                    if (ol.size()==5) {
                        ol.remove(0);
                    }
                }   
            });
        } 

For loop is defined to loop infinitely (probably not the right way to solve this problem also) and I can see from console that circles are removed and added during the program run. Alas, I can't see GUI anymore.

1
0
4/22/2012 7:57:59 PM

Accepted Answer

In your code, you have some mistakes:

  • the GUI is not shown because, the execution flow never reaches the primaryStage.show(); due to infinite loop in the init(primaryStage);.
  • new ListChangeListener is added again and again in a loop. However you should add it only once in normal situations.
  • You are manipulating the ol (ol.remove(0);) in its own listener which triggers the new change event recursively.

As a solution: periodic tasks, long-time background executions can be separated to a different thread.

    @Override
    public void start(Stage primaryStage) throws Exception {
        Group root = new Group();
        primaryStage.setResizable(false);
        primaryStage.setScene(new Scene(root, sceneWidth, sceneHeight));

        final ObservableList<Node> ol = root.getChildren();

        new Thread(new Runnable() {
            @Override public void run() {
                while (true) {
                    try {
                        // Wait for 2 seconds.
                        Thread.sleep(2000);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    Platform.runLater(new Runnable() {
                        @Override public void run() {
                            System.out.println("ol size:" + ol.size());
                            if (ol.size() == 5) {
                                ol.remove(0);
                            }
                            ol.add(createCircle());
                        }
                    });
                }
            }
        }).start();
        primaryStage.show();
    }  

I have only changed the content of start(Stage primaryStage). There is no need to add a listener. This solution is very quick but not elegant way. You must manage the thread yourself. For more elegant and modern approach refer to Worker Threading in JavaFX 2.0.
In addition, if you really want a real animation then see the example Colorful Circles Application.

2
4/23/2012 12:33:35 PM

A similar question was also asked on the on the Oracle forums last year.

Here is sample solution using Timeline, which I prefer to a solution relying on worker threading. Though both can get the job done, I find using the JavaFX animation APIs more elegant and less error prone.

import javafx.animation.*;
import javafx.application.Application;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.Random;

public class FiveAutoCircleExample extends Application {
  private static final Random r = new Random();
  public static final int SCENE_SIZE = 800;

  public static void main(String[] args) throws Exception { launch(args); }
  public void start(final Stage stage) throws Exception {
    final Group circles = new Group();
    final Timeline animation = new Timeline(
      new KeyFrame(Duration.seconds(.5), 
      new EventHandler<ActionEvent>() {
        @Override public void handle(ActionEvent actionEvent) {
          while (circles.getChildren().size() >= 5) circles.getChildren().remove(0);
          int radius = 10 * r.nextInt(20);
          circles.getChildren().add(
            new Circle(
              r.nextInt(SCENE_SIZE - radius * 2) + radius, r.nextInt(SCENE_SIZE - radius * 2) + radius,
              radius,
              new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), r.nextDouble())
            )
          );
        }
      })
    );
    animation.setCycleCount(Animation.INDEFINITE);
    animation.play();

    // display the scene.
    stage.setScene(new Scene(circles, SCENE_SIZE, SCENE_SIZE, Color.CORNSILK));
    stage.show();
  }
}

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon