Issue
I want to make a notification window with animated text. A notification would be sent by a button click and the animation would start playing. My problem is that when I click the button again before the previous animation is done, two animations get executed at once. How do I make each method call of "sendMessage()" wait for the other to finish? If it has any significance there are multiple nodes that call the sendMessage() method in my program unlike in my MRE, so I want some kind of Queue with messages. Here is my MRE:
public class AnimationTest extends Application {
private final Label messageLabel = new Label();
@Override
public void start(Stage stage) throws IOException {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
Scene scene = new Scene(vBox, 320, 240);
vBox.getChildren().add(messageLabel);
Button button = new Button();
button.setOnAction(event -> sendMessage("Some animated text."));
vBox.getChildren().add(button);
stage.setScene(scene);
stage.show();
}
private void sendMessage(String message) {
final IntegerProperty i = new SimpleIntegerProperty(0);
Timeline timeline = new Timeline();
KeyFrame keyFrame = new KeyFrame(
Duration.millis(40),
event -> {
if (i.get() > message.length()) {
timeline.stop();
} else {
messageLabel.setText(message.substring(0, i.get()));
i.set(i.get() + 1);
}
});
timeline.getKeyFrames().add(keyFrame);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
public static void main(String[] args) {
launch();
}
}
Solution
For the specific example you posted, the easiest approach is to disable the button immediately prior to starting the animation, and enable it again when the animation stops. Here is one way to do this:
public class AnimationTest extends Application {
private final Label messageLabel = new Label();
@Override
public void start(Stage stage) {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
Scene scene = new Scene(vBox, 320, 240);
vBox.getChildren().add(messageLabel);
Button button = new Button();
button.setOnAction(event -> {
Animation animation = sendMessage("Some animated text.");
button.disableProperty().bind(Bindings.equal(animation.statusProperty(), Animation.Status.RUNNING));
});
vBox.getChildren().add(button);
stage.setScene(scene);
stage.show();
}
private Animation sendMessage(String message) {
final IntegerProperty i = new SimpleIntegerProperty(0);
Timeline timeline = new Timeline();
KeyFrame keyFrame = new KeyFrame(
Duration.millis(40),
event -> {
if (i.get() > message.length()) {
timeline.stop();
} else {
messageLabel.setText(message.substring(0, i.get()));
i.set(i.get() + 1);
}
});
timeline.getKeyFrames().add(keyFrame);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
return timeline ;
}
public static void main(String[] args) {
launch();
}
}
If you want to allow these messages to accumulate in a queue, and a new animation to start when the old one finishes, you need to keep a queue of the messages and a reference to a current animation that's running (if there is one). You can poll the queue from an AnimationTimer
and start a new animation when a new message appears, if there is no current animation running.
I'd recommend thinking about whether this is the approach you want to take; there's no guarantee here that your messages will not appear more quickly than they can be animated, in which case the queue will grow indefinitely. However, this is an implementation if you can otherwise assure that this is not the case:
public class AnimationTest extends Application {
private final Label messageLabel = new Label();
private final Queue<String> messages = new LinkedList<>();
private Animation currentAnimation = null ;
@Override
public void start(Stage stage) {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
Scene scene = new Scene(vBox, 320, 240);
vBox.getChildren().add(messageLabel);
Button button = new Button();
button.setOnAction(event -> messages.add("Some animated text."));
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long l) {
if (currentAnimation == null || currentAnimation.getStatus() == Animation.Status.STOPPED) {
String message = messages.poll();
if (message != null) {
currentAnimation = sendMessage(message);
currentAnimation.play();
}
}
}
};
timer.start();
vBox.getChildren().add(button);
stage.setScene(scene);
stage.show();
}
private Animation sendMessage(String message) {
final IntegerProperty i = new SimpleIntegerProperty(0);
Timeline timeline = new Timeline();
KeyFrame keyFrame = new KeyFrame(
Duration.millis(40),
event -> {
if (i.get() > message.length()) {
timeline.stop();
} else {
messageLabel.setText(message.substring(0, i.get()));
i.set(i.get() + 1);
}
});
timeline.getKeyFrames().add(keyFrame);
timeline.setCycleCount(Animation.INDEFINITE);
return timeline ;
}
public static void main(String[] args) {
launch();
}
}
Note there are no threading considerations here. The handle()
method is invoked on the FX Application Thread, so the only requirement is that the messages are placed in the queue on the same thread. This happens in this example because the button's event handler is invoked on that thread. If your messages are coming from a background thread, you should ensure they are added to the queue on the FX Application Thread, either by using Platform.runLater(...)
or (preferably) by using the JavaFX Concurrency API (i.e. by retrieving the messages in a Task
or Service
and adding them to the queue in an onSucceeded
handler).
Answered By - James_D
Answer Checked By - Senaida (JavaFixing Volunteer)