Issue
I'm creating a simple memory game with cards image: user tries to find two card with the same image. For start I create a button with image of closed card. When user clicks on button, program changes image of picture on picture of open card, wait 2 seconds and changes picture back. And I stucked on it.
When user click on button, I run three another threads in action listener of the button: fist changes closed picture on open picture, second waits 2 seconds, third changes picture back. But instead of it, the changes in the ui occur only when all threads exit
Code of action list
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
Runnable r1 = ()->{
synchronized (btn) {
System.out.println("test");
Image image2 = new Image(getClass().getResourceAsStream("img/open.jpg"));
ImageView iv = new ImageView(image2);
iv.setFitWidth(100);
iv.setFitHeight(100);
Platform.runLater(()->btn.setGraphic(iv));
}
};
Runnable r2 = ()->{
synchronized (btn) {
System.out.println("test2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 end");
}
};
Runnable r3=()->{
synchronized (btn) {
System.out.println("test3");
Image image2 = new Image(getClass().getResourceAsStream("img/unnamed.jpg"));
ImageView iv = new ImageView(image2);
iv.setFitWidth(100);
iv.setFitHeight(100);
btn.setGraphic(iv);
}
};
Platform.runLater(r1);
Platform.runLater(r2);
Platform.runLater(r3);
}
});
Solution
As pointed out in the comments, you're not actually creating any threads at all here; all you're doing is scheduling three chunks of code to run on the FX Application Thread (the thread you're currently on) at some point in the future, one after the other. One of those chunks of code pauses the FX Application Thread for two seconds, which will prevent it from doing what it's supposed to be doing (rendering the UI and handling user events) during that time. Hence you don't actually see the results of the code in r1
, because the UI isn't rendered during the pause created by r2
, and then r3
is immediately executed.
All you need here is to execute the code in r1
, and then to execute the code in r3
after two seconds. The easiest way to accomplish this is with a PauseTransition
:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
Image image2 = new Image(getClass().getResourceAsStream("img/open.jpg"));
ImageView iv = new ImageView(image2);
iv.setFitWidth(100);
iv.setFitHeight(100);
btn.setGraphic(iv);
PauseTransition pause = new PauseTransition(Duration.seconds(2));
pause.setOnFinished(e -> {
Image image2 = new Image(getClass().getResourceAsStream("img/unnamed.jpg"));
ImageView iv = new ImageView(image2);
iv.setFitWidth(100);
iv.setFitHeight(100);
btn.setGraphic(iv);
});
pause.play();
}
});
There's also no real reason to create two ImageView
s; you can just update the image, and no need to reload the images every time the button is pressed (you can use the same image in multiple image views, if you need). And if you use lambda expressions for your event handler, your code simplifies to
// you can scope these as widely as you need: you should only load them once
Image openImage = new Image(getClass().getResourceAsStream("img/open.jpg"));
Image unnamedImage = new Image(getClass().getResourceAsStream("img/unnamed.jpg"));
// this needs to be specific to this button:
ImageView iv = new ImageView();
iv.setFitWidth(100);
iv.setFitHeight(100);
btn.setOnAction(event -> {
iv.setImage(openImage);
btn.setGraphic(iv);
PauseTransition pause = new PauseTransition(Duration.seconds(2));
pause.setOnFinished(e -> iv.setImage(unnamedImage));
pause.play();
});
Answered By - James_D