Synchronize transition across multiple control instances - java

I have several custom control. One of the requirements is to have the control be able to flash based on it's state. I am using a fade transition, which works great.
How can I sync the fade transition so that if multiple controls are flashing on the screen, they are fading in and out at the same rate?
The only thing I can think of to make this work easily is to have a static ParallelTransition that each instance adds it's fade transition to when active but that just doesn't sound like a clean approach to me.

You could use binding link everything to a key value which changes based upon a timeline.
The following code will create a bunch of circles, then fire off blinking on them at various times, but once the blinking starts, all of the circles continue blinking in unison (don't run this if you are prone to epilepsy...)
You can play around with the values and the interpolators used in the timeline to get the effect you want.
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Random;
public class Synchronicity extends Application {
private static final double N = 10;
private static final double R = 10;
private static final Duration D = Duration.seconds(2);
private static final double MIN_VAL = 0.1;
private static final double MAX_VAL = 1;
private static final Random r = new Random();
private final DoubleProperty opacity = new SimpleDoubleProperty(MAX_VAL);
private final Timeline oscillator = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, MAX_VAL, Interpolator.EASE_BOTH)),
new KeyFrame(D.divide(2), new KeyValue(opacity, MIN_VAL, Interpolator.EASE_BOTH))
);
#Override
public void start(final Stage stage) {
Pane layout = new Pane();
for (int i = 0; i < N; i++) {
Circle circle = new Circle(R, Color.FIREBRICK);
circle.setCenterX(2 * R + i * R * 3);
circle.setCenterY(R * 2);
layout.getChildren().add(circle);
PauseTransition pause = new PauseTransition(D.multiply(r.nextDouble() * N));
pause.setOnFinished(e -> blink(circle));
pause.play();
}
layout.setMinSize(R + N * R * 3,R * 4);
stage.setScene(new Scene(layout));
stage.show();
oscillator.setAutoReverse(true);
oscillator.setCycleCount(Timeline.INDEFINITE);
oscillator.play();
}
private void blink(Node node) {
node.opacityProperty().bind(opacity);
}
public static void main(String[] args) {
launch(args);
}
}

Related

How to add a timestamp in java on a pane

I am currently working on this assignment and I can not seem to get this program to run even though I don't have any errors really popping up ? I am trying to add a time stamp to the pane as well but every time I add the "ts" name for the time stamp to the Pane or Hbox's get children code it goes red.. I am not sure what exactly I'm doing wrong if anyone can point me in the right direction id greatly appreciate it...
package PCK1;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import java.sql.Timestamp;
import java.util.Date;
import java.text.SimpleDateFormat;
public class MainClass
{
public static void start(Stage stage)
{
// Time Stamp
Date date = new Date();
Timestamp ts=new Timestamp(date.getTime());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter.format(ts));
//Create a Circle
Circle c1 = new Circle(75,100,20);
//Create a Pane
Pane p = new Pane();
p.setMinSize(100, 150);
p.setBackground(new Background(new BackgroundFill( Color.rgb(190, 220, 190), null, null)
));
p.getChildren().addAll(c1);
//Create a Button
Button btnUp = new Button("Up");
btnUp.setOnAction((ActionEvent e) -> {double y = c1.getCenterY();
y -= 20.0;
c1.setCenterY(y);
});
Button btnDown = new Button("Down");
btnDown.setOnAction((ActionEvent e) -> {double y = c1.getCenterY();
y += 20.0;
c1.setCenterY(y);
});
//Create a HBox
HBox hb = new HBox();
hb.getChildren().addAll(btnUp, btnDown, p, ts);
hb.setBackground(new Background(new BackgroundFill(Color.rgb(150,200,150),null,null)));
hb.setMinSize(100, 50);
hb.setPadding(new Insets(10,10,10,10));
Scene scene = new Scene(hb);
stage.setScene(scene);
stage.setTitle("JavaFx");
stage.setWidth(250);
stage.setHeight(250);
stage.show();
}
}
Answer for displaying a timestamp
Specifically, for the timestamp question, see the following example code:
private Label createTimestampLabel() {
LocalDateTime now = LocalDateTime.now();
String formattedTimestamp = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return new Label(formattedTimestamp);
}
It uses the java.time APIs explained in the Oracle Date Time tutorial to get the current time from LocalDateTime and format it as a String using a standard format.
It sets the formatted timestamp string as the text of a Label node.
Now that the returned element is a Node, it can be placed in the scene graph without generating the compile error you saw in your original example.
Using the java.time APIs is preferred over the java.sql.Timestamp and java.util.Date code in your question. You are not working with SQL, so you should not be using java.sql.Timestamp. The java.time classes also have many improvements over obsolete date and time functions used in other Java packages like java.util.
Answer in context with a re-write of your example code
There were a lot of things about the provided example application that were either wrong or annoyed me.
So I re-wrote it to match a bit more closely how I would normally write such an application.
There are maybe a hundred different small decisions made in the choices for how to implement the re-write and explaining them all here would be too verbose.
Hopefully, you can compare the re-write to your original code, note some of the differences, and learn some things from it.
GraphicControlApp.java
package org.example.javafx.demo.graphiccontrol;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class GraphicControlApp extends Application {
public void start(Stage stage) {
GraphicController graphicController = new GraphicController();
Scene scene = new Scene(graphicController.getUI());
stage.setScene(scene);
stage.setTitle("JavaFX Interactive Graphic Control Demonstration");
stage.show();
}
}
GraphicController.java
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* UI creator and controller for application logic.
*
* Normally, most UI elements would be defined externally in FXML,
* however, for a simple application, we define the UI via private functions in this class.
*/
public class GraphicController {
// amount to move the circle across the surface on interaction.
private static final double MOVEMENT_DELTA = 20.0;
// default spacing between UI elements.
private static final double SPACING = 10;
// normally the styles would be configured in an external css stylesheet,
// but we place the background definitions here for a simple application.
private static final Color SURFACE_COLOR = Color.rgb(190, 220, 190);
private static final Background surfaceBackground = createBackground(SURFACE_COLOR);
private static final Color APP_BACKGROUND_COLOR = Color.rgb(150, 200, 150);
private static final Background appBackground = createBackground(APP_BACKGROUND_COLOR);
private Button up;
private Button down;
/**
* #return the complete layout for the application with event handlers attached for logic control.
*/
public Pane getUI() {
Circle circle = new Circle(75, 100, 20);
Pane surface = createSurface(circle);
HBox controls = createControls(circle);
Label timestampLabel = createTimestampLabel();
Pane layout = createLayout(surface, controls, timestampLabel);
attachKeyboardHandlers(layout);
return layout;
}
/**
* Create a label formatted with the current time in ISO standard format (e.g. '2011-12-03T10:15:30')
*
* #return label with the current timestamp.
*/
private Label createTimestampLabel() {
LocalDateTime now = LocalDateTime.now();
String formattedTimestamp = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return new Label(formattedTimestamp);
}
/**
* Create a surface on which a circle can move.
*
* #param circle the circle which can move on the surface.
* #return the created surface.
*/
private Pane createSurface(Circle circle) {
Pane surface = new Pane();
surface.setMinSize(100, 150);
surface.setBackground(surfaceBackground);
surface.getChildren().addAll(circle);
// we must define a clip on the surface to ensure that elements
// in the surface do not render outside the surface.
Rectangle clip = new Rectangle();
clip.widthProperty().bind(surface.widthProperty());
clip.heightProperty().bind(surface.heightProperty());
surface.setClip(clip);
return surface;
}
private VBox createLayout(Pane surface, HBox controls, Label timestampLabel) {
VBox layout = new VBox(SPACING, controls, surface, timestampLabel);
layout.setBackground(appBackground);
layout.setPadding(new Insets(SPACING));
VBox.setVgrow(surface, Priority.ALWAYS);
return layout;
}
/**
* Create controls which can control the movement of a circle.
*
* #param circle the circle which can be controlled
* #return the created controls with handlers attached for circle movement control.
*/
private HBox createControls(Circle circle) {
up = new Button("Up");
up.setOnAction(e -> moveVertically(circle, -MOVEMENT_DELTA));
down = new Button("Down");
down.setOnAction(e -> moveVertically(circle, MOVEMENT_DELTA));
return new HBox(SPACING, up, down);
}
private void moveVertically(Circle circle, double delta) {
double y = circle.getCenterY();
// we only restrict movement in the up direction,
// but allow unlimited movement in the down direction
// (even if that movement would mean that the circle would extend totally
// outside the current visible boundary of the surface).
if ((y + delta) < 0) {
return;
}
circle.setCenterY(y + delta);
}
/**
* Adds standard keyboard handling logic to the UI.
*
* Handlers are attached to the relevant scene whenever
* the scene containing the UI changes.
*
* #param layout the UI which will respond to keyboard input.
*/
private void attachKeyboardHandlers(Pane layout) {
EventHandler<KeyEvent> keyEventHandler = event -> {
switch (event.getCode()) {
case UP -> { up.requestFocus(); up.fire(); }
case DOWN -> { down.requestFocus(); down.fire(); }
}
};
layout.sceneProperty().addListener((observable, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.removeEventFilter(
KeyEvent.KEY_PRESSED,
keyEventHandler
);
}
if (newScene != null) {
newScene.addEventFilter(
KeyEvent.KEY_PRESSED,
keyEventHandler
);
}
});
}
private static Background createBackground(Color surfaceColor) {
return new Background(new BackgroundFill(surfaceColor, null, null));
}
}
You should show the timestamp as text with the TextField (Doc) :
TextField myText = new TextField();
myText.setText("Time: " + formatter.format(ts));
// set what you want to the TextField object: padding, size, color etc...
p.getChildren().addAll(myText);

Connecting a Slider and Spinner that has a StringConverter

For numeric input, it's sometimes convenient to synchronize an analog control to a text display. In this Swing example, a JSpinner and a JSlider each listen for change events, and each updates the other's model to match. A similar JavaFX program, shown below, connects a Spinner and a Slider, and these listeners keep the controls coordinated:
slider.valueProperty().addListener((Observable o) -> {
spinner.getValueFactory().setValue(slider.getValue());
});
spinner.valueProperty().addListener((Observable o) -> {
slider.setValue((double) spinner.getValue());
});
Unfortunately, when I added a StringConverter to the spinner's SpinnerValueFactory, the initial value was unformatted until either control was changed—even when setting the initial value explicitly again, after adding the converter:
spinner.getValueFactory().setConverter(…);
spinner.getValueFactory().setValue(INITIAL_VALUE);
Where am I going wrong?
import java.text.NumberFormat;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.converter.PercentageStringConverter;
/**
* #see https://stackoverflow.com/a/6067986/230513
*/
public class SpinSlider extends Application {
private static final double MIN = 0;
private static final double MAX = 1;
private static final double INITIAL_VALUE = 0.5;
private static final double STEP_INCREMENT = 0.1;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SpinSlider");
Slider slider = new Slider(MIN, MAX, INITIAL_VALUE);
slider.setBlockIncrement(STEP_INCREMENT);
Spinner spinner = new Spinner(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
spinner.getValueFactory().setConverter(
new PercentageStringConverter(NumberFormat.getPercentInstance()));
spinner.getValueFactory().setValue(INITIAL_VALUE);
slider.valueProperty().addListener((Observable o) -> {
spinner.getValueFactory().setValue(slider.getValue());
});
spinner.valueProperty().addListener((Observable o) -> {
slider.setValue((double) spinner.getValue());
});
GridPane root = new GridPane();
root.addRow(0, slider, spinner);
root.setPadding(new Insets(8, 8, 8, 8));
root.setHgap(8);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Your INITIAL_VALUE is first used as the initialValue parameter to the spinner's constructor. Internally, the spinner's concrete SpinnerValueFactory is a DoubleSpinnerValueFactory that adds a ChangeListener to its value property; when the initial value is set again, the value hasn't really changed. Two approaches suggest themselves:
Specify a different value to the constructor and set the desired one after adding the converter:
Spinner spinner = new Spinner(MIN, MAX, -42, STEP_INCREMENT);
spinner.getValueFactory().setConverter(…);
spinner.getValueFactory().setValue(INITIAL_VALUE);
Construct a SpinnerValueFactory with the desired initial value and use it to construct the spinner:
SpinnerValueFactory factory = new SpinnerValueFactory
.DoubleSpinnerValueFactory(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
factory.setConverter(…);
Spinner spinner = new Spinner(factory);
In addition, the example below replaces the two listeners with a bidirectional binding, which uses weak listeners to allow garbage collection of properties:
slider.valueProperty().bindBidirectional(
spinner.getValueFactory().valueProperty());
Trivially, the spinner and slider can control each other. More commonly, each can stay synchronized in the course of controlling a property held by another model:
model.xProperty().bindBidirectional(slider.valueProperty());
model.xProperty().bindBidirectional(spinner.getValueFactory().valueProperty());
import java.text.NumberFormat;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.converter.PercentageStringConverter;
/**
* #see https://stackoverflow.com/a/55427307/230513
* #see https://stackoverflow.com/a/6067986/230513
*/
public class SpinSlider extends Application {
private static final double MIN = 0;
private static final double MAX = 1;
private static final double INITIAL_VALUE = 0.5;
private static final double STEP_INCREMENT = 0.1;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SpinSlider");
Slider slider = new Slider(MIN, MAX, INITIAL_VALUE);
slider.setBlockIncrement(STEP_INCREMENT);
SpinnerValueFactory factory = new SpinnerValueFactory
.DoubleSpinnerValueFactory(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
factory.setConverter(new PercentageStringConverter(
NumberFormat.getPercentInstance()));
Spinner spinner = new Spinner(factory);
slider.valueProperty().bindBidirectional(spinner.getValueFactory().valueProperty());
GridPane root = new GridPane();
root.addRow(0, slider, spinner);
root.setPadding(new Insets(8, 8, 8, 8));
root.setHgap(8);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

How do I insert all my shapes into an array?

I am Trying to build a cinema booker, where I have to select the seats. I was thinking about make placing all my rectangles in an array so i could use it for later, when clicking on a seat it should check if left and right seats are booked. Here the array index should help me. However I cant figure out how to get to this stage. (See picture.)
Take this scenario:
You click on a rectangle (representing a seat). It changes color only it is not Red colored. So Seats[][].checkNeighbourColor or something like that.See picture
package sample;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.awt.*;
import java.util.ArrayList;
public class Main extends Application {
private int seats = 12;
private int rows = 8;
private static Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); //gets screen resolution data
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("");
Group root = new Group();
Scene scene = new Scene(root, screenSize.getWidth()/4, screenSize.getHeight()/3, Color.WHITE);
for (int i = 0; i<= seats; i++)
{
Rectangle r = new Rectangle();
r.setFill(Color.GREEN);
r.setX(scene.getWidth()/5+i*30);
r.setY(scene.getHeight()/5);
r.setWidth(screenSize.getHeight()/80);
r.setHeight(screenSize.getHeight()/80);
root.getChildren().add(r);
for (int q = 0; q<=rows; q++)
{
Rectangle s = new Rectangle();
s.setFill(Color.GREEN);
s.setX(scene.getWidth()/5+i*30);
s.setY(scene.getHeight()/5+q*30);
s.setWidth(screenSize.getHeight()/80);
s.setHeight(screenSize.getHeight()/80);
root.getChildren().add(s);
s.setOnMouseClicked(event ->{
s.setFill(Color.BLACK);
});
}
}
primaryStage.setScene(scene);
primaryStage.show();
}
}
It's not entirely clear to me what you're asking, but can't you just do
private Rectangle[][] rectangles = new Rectangle[seats][rows];
and then just do
rectangles[i][q] = s ;
For your listener you can do
final int row = i ;
final int seat = q ;
s.setOnMouseClicked(event -> {
// check rectangles[row-1][seat] and rectangles[row+1][seat] as needed,
// checking for range of row first
});
Aside: don't mix AWT and JavaFX. Use the Screen API to get the dimension of the physical screen(s) in JavaFX.
Okay I found my mistake. It was a simple ArrayOutOfBound (The statement in my for-loops was wrong), but thank you all for your help :)

Heavy rendering task (in canvas) in JavaFX blocks GUI

I want to create an application that performs many renderings in a canvas.
The normal JavaFX way blocks the GUI: It is realy hard to press the button in the application code below (run with Java 8).
I searched the web, but JavaFX does not support background rendering: All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later. So I cannot even use two canvases and exchange then after rendering.
Also the javafx.scene.Node.snapshot(SnapshotParameters, WritableImage) cannot be used to create an image in a background thread, as it needs to run inside the JavaFX application thread and so it will block the GUI also.
Any ideas to have a non blocking GUI with many rendering operations? (I just want to press buttons etc. while the rendering is performed somehow in background or paused regularly)
package canvastest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class DrawLinieTest extends Application
{
int interations = 2;
double lineSpacing = 1;
Random rand = new Random(666);
List<Color> colorList;
final VBox root = new VBox();
Canvas canvas = new Canvas(1200, 800);
Canvas canvas2 = new Canvas(1200, 800);
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<?> drawShapesFuture;
{
colorList = new ArrayList<>(256);
colorList.add(Color.ALICEBLUE);
colorList.add(Color.ANTIQUEWHITE);
colorList.add(Color.AQUA);
colorList.add(Color.AQUAMARINE);
colorList.add(Color.AZURE);
colorList.add(Color.BEIGE);
colorList.add(Color.BISQUE);
colorList.add(Color.BLACK);
colorList.add(Color.BLANCHEDALMOND);
colorList.add(Color.BLUE);
colorList.add(Color.BLUEVIOLET);
colorList.add(Color.BROWN);
colorList.add(Color.BURLYWOOD);
}
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage primaryStage)
{
primaryStage.setTitle("Drawing Operations Test");
System.out.println("Init...");
// inital draw that creates a big internal operation buffer (GrowableDataBuffer)
drawShapes(canvas.getGraphicsContext2D(), lineSpacing);
drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);
System.out.println("Start testing...");
new CanvasRedrawTask().start();
Button btn = new Button("test " + System.nanoTime());
btn.setOnAction((ActionEvent e) ->
{
btn.setText("test " + System.nanoTime());
});
root.getChildren().add(btn);
root.getChildren().add(canvas);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void drawShapes(GraphicsContext gc, double f)
{
System.out.println(">>> BEGIN: drawShapes ");
gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
gc.setLineWidth(10);
gc.setLineCap(StrokeLineCap.ROUND);
long time = System.nanoTime();
double w = gc.getCanvas().getWidth() - 80;
double h = gc.getCanvas().getHeight() - 80;
int c = 0;
for (int i = 0; i < interations; i++)
{
for (double x = 0; x < w; x += f)
{
for (double y = 0; y < h; y += f)
{
gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
c++;
}
}
}
System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms");
}
public synchronized void drawShapesAsyc(final double f)
{
if (drawShapesFuture != null && !drawShapesFuture.isDone())
return;
drawShapesFuture = executorService.submit(() ->
{
drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);
Platform.runLater(() ->
{
root.getChildren().remove(canvas);
Canvas t = canvas;
canvas = canvas2;
canvas2 = t;
root.getChildren().add(canvas);
});
});
}
class CanvasRedrawTask extends AnimationTimer
{
long time = System.nanoTime();
#Override
public void handle(long now)
{
drawShapesAsyc(lineSpacing);
long f = (System.nanoTime() - time) / 1000 / 1000;
System.out.println("Time since last redraw " + f + " ms");
time = System.nanoTime();
}
}
}
EDIT Edited the code to show that a background thread that sends the draw operations and than exchange the canvas does not resolve the problem! Because All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later.
You are drawing 1.6 million lines per frame. It is simply a lot of lines and takes time to render using the JavaFX rendering pipeline. One possible workaround is not to issue all drawing commands in a single frame, but instead render incrementally, spacing out drawing commands, so that the application remains relatively responsive (e.g. you can close it down or interact with buttons and controls on the app while it is rendering). Obviously, there are some tradeoffs in extra complexity with this approach and the result is not as desirable as simply being able to render extremely large amounts of draw commands within the context of single 60fps frame. So the presented approach is only acceptable for some kinds of applications.
Some ways to perform an incremental render are:
Only issue a max number of calls each frame.
Place the rendering calls into a buffer such as a blocking queue and just drain a max number of calls each frame from the queue.
Here is a sample of the first option.
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.concurrent.*;
import javafx.scene.Scene;
import javafx.scene.canvas.*;
import javafx.scene.control.Button;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.*;
public class DrawLineIncrementalTest extends Application {
private static final int FRAME_CALL_THRESHOLD = 25_000;
private static final int ITERATIONS = 2;
private static final double LINE_SPACING = 1;
private final Random rand = new Random(666);
private List<Color> colorList;
private final WritableImage image = new WritableImage(ShapeService.W, ShapeService.H);
private final Lock lock = new ReentrantLock();
private final Condition rendered = lock.newCondition();
private final ShapeService shapeService = new ShapeService();
public DrawLineIncrementalTest() {
colorList = new ArrayList<>(256);
colorList.add(Color.ALICEBLUE);
colorList.add(Color.ANTIQUEWHITE);
colorList.add(Color.AQUA);
colorList.add(Color.AQUAMARINE);
colorList.add(Color.AZURE);
colorList.add(Color.BEIGE);
colorList.add(Color.BISQUE);
colorList.add(Color.BLACK);
colorList.add(Color.BLANCHEDALMOND);
colorList.add(Color.BLUE);
colorList.add(Color.BLUEVIOLET);
colorList.add(Color.BROWN);
colorList.add(Color.BURLYWOOD);
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Drawing Operations Test");
System.out.println("Start testing...");
new CanvasRedrawHandler().start();
Button btn = new Button("test " + System.nanoTime());
btn.setOnAction(e -> btn.setText("test " + System.nanoTime()));
Scene scene = new Scene(new VBox(btn, new ImageView(image)));
primaryStage.setScene(scene);
primaryStage.show();
}
private class CanvasRedrawHandler extends AnimationTimer {
long time = System.nanoTime();
#Override
public void handle(long now) {
if (!shapeService.isRunning()) {
shapeService.reset();
shapeService.start();
}
if (lock.tryLock()) {
try {
System.out.println("Rendering canvas");
shapeService.canvas.snapshot(null, image);
rendered.signal();
} finally {
lock.unlock();
}
}
long f = (System.nanoTime() - time) / 1000 / 1000;
System.out.println("Time since last redraw " + f + " ms");
time = System.nanoTime();
}
}
private class ShapeService extends Service<Void> {
private Canvas canvas;
private static final int W = 1200, H = 800;
public ShapeService() {
canvas = new Canvas(W, H);
}
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
drawShapes(canvas.getGraphicsContext2D(), LINE_SPACING);
return null;
}
};
}
private void drawShapes(GraphicsContext gc, double f) throws InterruptedException {
lock.lock();
try {
System.out.println(">>> BEGIN: drawShapes ");
gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
gc.setLineWidth(10);
gc.setLineCap(StrokeLineCap.ROUND);
long time = System.nanoTime();
double w = gc.getCanvas().getWidth() - 80;
double h = gc.getCanvas().getHeight() - 80;
int nCalls = 0, nCallsPerFrame = 0;
for (int i = 0; i < ITERATIONS; i++) {
for (double x = 0; x < w; x += f) {
for (double y = 0; y < h; y += f) {
gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
nCalls++;
nCallsPerFrame++;
if (nCallsPerFrame >= FRAME_CALL_THRESHOLD) {
System.out.println(">>> Pausing: drawShapes ");
rendered.await();
nCallsPerFrame = 0;
System.out.println(">>> Continuing: drawShapes ");
}
}
}
}
System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms for " + nCalls + " ops");
} finally {
lock.unlock();
}
}
}
}
Note that for the sample, it is possible to interact with the scene by clicking the test button while the incremental rendering is in progress. If desired, you could further enhance this to double buffer the snapshot images for the canvas so that the user doesn't see the incremental rendering. Also because the incremental rendering is in a Service, you can use the service facilities to track rendering progress and relay that to the UI via a progress bar or whatever mechanisms you wish.
For the above sample you can play around with the FRAME_CALL_THRESHOLD setting to vary the maximum number of calls which are issued each frame. The current setting of 25,000 calls per frame keeps the UI very responsive. A setting of 2,000,000 would be the same as fully rendering the canvas in a single frame (because you are issuing 1,600,000 calls in the frame) and no incremental rendering will be performed, however the UI will not be responsive while the rendering operations are being completed for that frame.
Side Note
There is something weird here. If you remove all of the concurrency stuff and the double canvases in the code in the original question and just use a single canvas with all logic on the JavaFX application thread, the initial invocation of drawShapes takes 27 seconds, and subsequent invocations take less that a second, but in all cases the application logic is asking the system to perform the same task. I don't know why the initial call is so slow, it seems like a performance issue in the JavaFX canvas implementation to me, perhaps related to inefficient buffer allocation. If that is the case, then perhaps the JavaFX canvas implementation could be tweaked so that a hint for a suggested initial buffer size could be provided, so that it more efficiently allocates space for its internal growable buffer implementation. It might be something worth filing a bug or discussing it on the JavaFX developer mailing list. Also note that the issue of a very slow initial rendering of the canvas is only visible when you issue a very large number (e.g. > 500,000) of rendering calls, so it won't effect all applications.
The issue that is described here has also been discussed on the JavaFX mailing list some months ago in this thread
http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-September/017939.html
The proposed solution is similar to the one given by jewelsea.

How to force Java FX scene refresh?

I have an Java FX scene with a start button and several rectangles which represent the tiles of a map. I also have drawn a sphere which represents my explorer (it has to explore the map), but I am having difficulties with running the animation.
In my OnMouseClicked handler for the start button, I start an algorithm for exploring the map which changes the position of the sphere and the colors of the tiles which have been visited. The problem is that the scene won't update itself while the algorithm is running, so I only get to see how the final scene will look like (after the algorithm has stopped running). How can I force a scene update so I can see all the color changes sequentially?
Later edit:
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Test extends Application {
private static final double boxOuterSize = 50;
private static final double boxInnerSize = 48;
private static final double boxCornerRadius = 20;
private Stage applicationStage;
private Scene applicationScene;
private static double sceneWidth = 1024;
private static double sceneHeight = 800;
private static HBox container = new HBox();
private static Group root = new Group();
private Rectangle[] rectangles = new Rectangle[10];
#Override
public void start(Stage mainStage) throws Exception {
applicationStage = mainStage;
container.setSpacing(10);
container.setPadding(new Insets(10, 10, 10, 10));
try {
applicationScene = new Scene(container, sceneWidth, sceneHeight);
applicationScene.addEventHandler(EventType.ROOT,(EventHandler<? super Event>)this);
applicationScene.setFill(Color.WHITE);
} catch (Exception exception) {
System.out.println ("exception : "+exception.getMessage());
}
applicationStage.setTitle("HurtLockerRobot - Tema 3 IA");
applicationStage.getIcons().add(new Image("icon.png"));
applicationStage.setScene(applicationScene);
for(int i=0; i<10; i++) {
Rectangle r = new Rectangle();
r.setFill(Color.BLUE);
r.setX(i * boxOuterSize);
r.setY(0);
r.setWidth(boxInnerSize);
r.setHeight(boxInnerSize);
r.setArcHeight(boxCornerRadius);
r.setArcWidth(boxCornerRadius);
r.setSmooth(true);
rectangles[i] = r;
root.getChildren().add(rectangles[i]);
}
container.getChildren().add(root);
Button startButton = new Button("Start");
startButton.setOnMouseClicked(new EventHandler<Event>() {
#Override
public void handle(Event arg0) {
for(int i=0; i<10; i++) {
rectangles[i].setFill(Color.RED);
// TODO: some kind of scene refresh here
}
}
});
container.getChildren().add(startButton);
applicationStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Initially all the rectangles are blue. The behavior I want to obtain here is to see the rectangles changing colors sequentially. The problem is that I only get to see the end result (all the rectangles change their color at the same time).
This is an old question and it caught my eye since this is a very general issue faced by people new to JavaFX.
The problem that OP is facing is because he updates all the rectangles at once, without waiting.
OP can wait by either creating a new Thread, put the thread on sleep for an estimated seconds for every iteration of the loop and then update the color of the rectangle on JavaFX application thread by using Platform.runLater.
#Override
public void handle(Event arg0) {
new Thread(() -> {
for(int i=0; i<10; i++) {
try {
Thread.sleep(1000); // Wait for 1 sec before updating the color
} catch (InterruptedException e) {
e.printStackTrace();
}
int finalI = i;
Platform.runLater(() -> rectangles[finalI].setFill(Color.RED));// Update on JavaFX Application Thread
}
}).start();
The above snippet is more of a traditional way of doing things. If we want to use the "JavaFX" ways of doing things, we can achieve the same by using an Animation.
Below is a code snippet which will wait for x-seconds before changing the color of the rectangle. It doesn't need any extra thread since the wait is handled by PauseTransition applied for each rectangle.
startButton.setOnMouseClicked(new EventHandler<Event>() {
#Override
public void handle(Event arg0) {
for(int i=0; i<10; i++) {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(i));
int finalI = i;
pauseTransition.setOnFinished(event -> rectangles[finalI].setFill(Color.RED));
pauseTransition.play();
}
}
});
It creates a PauseTransition for each rectangle and depending on its index in the array rectangles, it waits for the same number of seconds before updating the color.
This is because of :
exception : Test cannot be cast to javafx.event.EventHandler
Well, I have no idea how Class cast exception came up.
Otherwise, to delay, you can use Thread.sleep().
UPDATE:
Its good to use AnimationTimer to create an animation, you don't need to refresh anything.
Here, I have done a short EG to show color rect using FillTransition.
CODE:
import javafx.animation.FillTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class NewFXMain1 extends Application {
private static final double boxOuterSize = 50;
private static final double boxInnerSize = 48;
private static final double boxCornerRadius = 20;
private Rectangle rectangles = new Rectangle();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("rect");
Button btn = new Button();
StackPane root = new StackPane();
Rectangle r = new Rectangle();
r.setFill(Color.BLUE);
r.setX(2 * boxOuterSize);
r.setY(0);
r.setWidth(boxInnerSize);
r.setHeight(boxInnerSize);
r.setArcHeight(boxCornerRadius);
r.setArcWidth(boxCornerRadius);
r.setSmooth(true);
r.localToScene(boxOuterSize, boxOuterSize);
rectangles = r;
root.getChildren().add(rectangles);
btn.setText("display");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
FillTransition ft = new FillTransition(Duration.millis(3000), rectangles, Color.RED, Color.BLUE);
ft.setCycleCount(4);
ft.setAutoReverse(true);
ft.play();
}
});
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}

Categories

Resources